-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class AccountController < ApplicationController
-
1
helper :custom_fields
-
1
include CustomFieldsHelper
-
-
# prevents login action to be filtered by check_if_login_required application scope filter
-
1
skip_before_filter :check_if_login_required
-
-
# Login request and validation
-
1
def login
-
242
if request.get?
-
121
logout_user
-
else
-
121
authenticate_user
-
end
-
rescue AuthSourceException => e
-
logger.error "An error occured when authenticating #{params[:username]}: #{e.message}"
-
render_error :message => e.message
-
end
-
-
# Log out current user and redirect to welcome page
-
1
def logout
-
4
logout_user
-
4
redirect_to home_url
-
end
-
-
# Lets user choose a new password
-
1
def lost_password
-
redirect_to(home_url) && return unless Setting.lost_password?
-
if params[:token]
-
@token = Token.find_by_action_and_value("recovery", params[:token].to_s)
-
if @token.nil? || @token.expired?
-
redirect_to home_url
-
return
-
end
-
@user = @token.user
-
unless @user && @user.active?
-
redirect_to home_url
-
return
-
end
-
if request.post?
-
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
-
if @user.save
-
@token.destroy
-
flash[:notice] = l(:notice_account_password_updated)
-
redirect_to signin_path
-
return
-
end
-
end
-
render :template => "account/password_recovery"
-
return
-
else
-
if request.post?
-
user = User.find_by_mail(params[:mail].to_s)
-
# user not found or not active
-
unless user && user.active?
-
flash.now[:error] = l(:notice_account_unknown_email)
-
return
-
end
-
# user cannot change its password
-
unless user.change_password_allowed?
-
flash.now[:error] = l(:notice_can_t_change_password)
-
return
-
end
-
# create a new token for password recovery
-
token = Token.new(:user => user, :action => "recovery")
-
if token.save
-
Mailer.lost_password(token).deliver
-
flash[:notice] = l(:notice_account_lost_email_sent)
-
redirect_to signin_path
-
return
-
end
-
end
-
end
-
end
-
-
# User self-registration
-
1
def register
-
redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration]
-
if request.get?
-
session[:auth_source_registration] = nil
-
@user = User.new(:language => Setting.default_language)
-
else
-
user_params = params[:user] || {}
-
@user = User.new
-
@user.safe_attributes = user_params
-
@user.admin = false
-
@user.register
-
if session[:auth_source_registration]
-
@user.activate
-
@user.login = session[:auth_source_registration][:login]
-
@user.auth_source_id = session[:auth_source_registration][:auth_source_id]
-
if @user.save
-
session[:auth_source_registration] = nil
-
self.logged_user = @user
-
flash[:notice] = l(:notice_account_activated)
-
redirect_to :controller => 'my', :action => 'account'
-
end
-
else
-
@user.login = params[:user][:login]
-
unless user_params[:identity_url].present? && user_params[:password].blank? && user_params[:password_confirmation].blank?
-
@user.password, @user.password_confirmation = user_params[:password], user_params[:password_confirmation]
-
end
-
-
case Setting.self_registration
-
when '1'
-
register_by_email_activation(@user)
-
when '3'
-
register_automatically(@user)
-
else
-
register_manually_by_administrator(@user)
-
end
-
end
-
end
-
end
-
-
# Token based account activation
-
1
def activate
-
redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
-
token = Token.find_by_action_and_value('register', params[:token])
-
redirect_to(home_url) && return unless token and !token.expired?
-
user = token.user
-
redirect_to(home_url) && return unless user.registered?
-
user.activate
-
if user.save
-
token.destroy
-
flash[:notice] = l(:notice_account_activated)
-
end
-
redirect_to signin_path
-
end
-
-
1
private
-
-
1
def authenticate_user
-
121
if Setting.openid? && using_open_id?
-
open_id_authenticate(params[:openid_url])
-
else
-
121
password_authentication
-
end
-
end
-
-
1
def password_authentication
-
121
user = User.try_to_login(params[:username], params[:password])
-
-
121
if user.nil?
-
invalid_credentials
-
121
elsif user.new_record?
-
onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id })
-
else
-
# Valid user
-
121
successful_authentication(user)
-
end
-
end
-
-
1
def open_id_authenticate(openid_url)
-
authenticate_with_open_id(openid_url, :required => [:nickname, :fullname, :email], :return_to => signin_url, :method => :post) do |result, identity_url, registration|
-
if result.successful?
-
user = User.find_or_initialize_by_identity_url(identity_url)
-
if user.new_record?
-
# Self-registration off
-
redirect_to(home_url) && return unless Setting.self_registration?
-
-
# Create on the fly
-
user.login = registration['nickname'] unless registration['nickname'].nil?
-
user.mail = registration['email'] unless registration['email'].nil?
-
user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
-
user.random_password
-
user.register
-
-
case Setting.self_registration
-
when '1'
-
register_by_email_activation(user) do
-
onthefly_creation_failed(user)
-
end
-
when '3'
-
register_automatically(user) do
-
onthefly_creation_failed(user)
-
end
-
else
-
register_manually_by_administrator(user) do
-
onthefly_creation_failed(user)
-
end
-
end
-
else
-
# Existing record
-
if user.active?
-
successful_authentication(user)
-
else
-
account_pending
-
end
-
end
-
end
-
end
-
end
-
-
1
def successful_authentication(user)
-
121
logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}"
-
# Valid user
-
121
self.logged_user = user
-
# generate a key and set cookie if autologin
-
121
if params[:autologin] && Setting.autologin?
-
set_autologin_cookie(user)
-
end
-
121
call_hook(:controller_account_success_authentication_after, {:user => user })
-
121
redirect_back_or_default :controller => 'my', :action => 'page'
-
end
-
-
1
def set_autologin_cookie(user)
-
token = Token.create(:user => user, :action => 'autologin')
-
cookie_name = Redmine::Configuration['autologin_cookie_name'] || 'autologin'
-
cookie_options = {
-
:value => token.value,
-
:expires => 1.year.from_now,
-
:path => (Redmine::Configuration['autologin_cookie_path'] || '/'),
-
:secure => (Redmine::Configuration['autologin_cookie_secure'] ? true : false),
-
:httponly => true
-
}
-
cookies[cookie_name] = cookie_options
-
end
-
-
# Onthefly creation failed, display the registration form to fill/fix attributes
-
1
def onthefly_creation_failed(user, auth_source_options = { })
-
@user = user
-
session[:auth_source_registration] = auth_source_options unless auth_source_options.empty?
-
render :action => 'register'
-
end
-
-
1
def invalid_credentials
-
logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}"
-
flash.now[:error] = l(:notice_account_invalid_creditentials)
-
end
-
-
# Register a user for email activation.
-
#
-
# Pass a block for behavior when a user fails to save
-
1
def register_by_email_activation(user, &block)
-
token = Token.new(:user => user, :action => "register")
-
if user.save and token.save
-
Mailer.register(token).deliver
-
flash[:notice] = l(:notice_account_register_done)
-
redirect_to signin_path
-
else
-
yield if block_given?
-
end
-
end
-
-
# Automatically register a user
-
#
-
# Pass a block for behavior when a user fails to save
-
1
def register_automatically(user, &block)
-
# Automatic activation
-
user.activate
-
user.last_login_on = Time.now
-
if user.save
-
self.logged_user = user
-
flash[:notice] = l(:notice_account_activated)
-
redirect_to :controller => 'my', :action => 'account'
-
else
-
yield if block_given?
-
end
-
end
-
-
# Manual activation by the administrator
-
#
-
# Pass a block for behavior when a user fails to save
-
1
def register_manually_by_administrator(user, &block)
-
if user.save
-
# Sends an email to the administrators
-
Mailer.account_activation_request(user).deliver
-
account_pending
-
else
-
yield if block_given?
-
end
-
end
-
-
1
def account_pending
-
flash[:notice] = l(:notice_account_pending)
-
redirect_to signin_path
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ActivitiesController < ApplicationController
-
1
menu_item :activity
-
1
before_filter :find_optional_project
-
1
accept_rss_auth :index
-
-
1
def index
-
@days = Setting.activity_days_default.to_i
-
-
if params[:from]
-
begin; @date_to = params[:from].to_date + 1; rescue; end
-
end
-
-
@date_to ||= Date.today + 1
-
@date_from = @date_to - @days
-
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
-
@author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
-
-
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
-
:with_subprojects => @with_subprojects,
-
:author => @author)
-
@activity.scope_select {|t| !params["show_#{t}"].nil?}
-
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
-
-
events = @activity.events(@date_from, @date_to)
-
-
if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, events.size, User.current, current_language])
-
respond_to do |format|
-
format.html {
-
@events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)}
-
render :layout => false if request.xhr?
-
}
-
format.atom {
-
title = l(:label_activity)
-
if @author
-
title = @author.name
-
elsif @activity.scope.size == 1
-
title = l("label_#{@activity.scope.first.singularize}_plural")
-
end
-
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
-
}
-
end
-
end
-
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
private
-
-
# TODO: refactor, duplicated in projects_controller
-
1
def find_optional_project
-
return true unless params[:id]
-
@project = Project.find(params[:id])
-
authorize
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class AdminController < ApplicationController
-
1
layout 'admin'
-
1
menu_item :projects, :only => :projects
-
1
menu_item :plugins, :only => :plugins
-
1
menu_item :info, :only => :info
-
-
1
before_filter :require_admin
-
1
helper :sort
-
1
include SortHelper
-
-
1
def index
-
1
@no_configuration_data = Redmine::DefaultData::Loader::no_data?
-
end
-
-
1
def projects
-
@status = params[:status] || 1
-
-
scope = Project.status(@status)
-
scope = scope.like(params[:name]) if params[:name].present?
-
-
@projects = scope.all(:order => 'lft')
-
-
render :action => "projects", :layout => false if request.xhr?
-
end
-
-
1
def plugins
-
1
@plugins = Redmine::Plugin.all
-
end
-
-
# Loads the default configuration
-
# (roles, trackers, statuses, workflow, enumerations)
-
1
def default_configuration
-
if request.post?
-
begin
-
Redmine::DefaultData::Loader::load(params[:lang])
-
flash[:notice] = l(:notice_default_data_loaded)
-
rescue Exception => e
-
flash[:error] = l(:error_can_t_load_default_data, e.message)
-
end
-
end
-
redirect_to :action => 'index'
-
end
-
-
1
def test_email
-
raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
-
# Force ActionMailer to raise delivery errors so we can catch it
-
ActionMailer::Base.raise_delivery_errors = true
-
begin
-
@test = Mailer.test_email(User.current).deliver
-
flash[:notice] = l(:notice_email_sent, User.current.mail)
-
rescue Exception => e
-
flash[:error] = l(:notice_email_error, e.message)
-
end
-
ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
-
redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
-
end
-
-
1
def info
-
@db_adapter_name = ActiveRecord::Base.connection.adapter_name
-
@checklist = [
-
[:text_default_administrator_account_changed, User.default_admin_account_changed?],
-
[:text_file_repository_writable, File.writable?(Attachment.storage_path)],
-
[:text_plugin_assets_writable, File.writable?(Redmine::Plugin.public_directory)],
-
[:text_rmagick_available, Object.const_defined?(:Magick)]
-
]
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'uri'
-
1
require 'cgi'
-
-
1
class Unauthorized < Exception; end
-
-
1
class ApplicationController < ActionController::Base
-
1
include Redmine::I18n
-
-
1
class_attribute :accept_api_auth_actions
-
1
class_attribute :accept_rss_auth_actions
-
1
class_attribute :model_object
-
-
1
layout 'base'
-
-
1
protect_from_forgery
-
1
def handle_unverified_request
-
super
-
cookies.delete(:autologin)
-
end
-
-
1
before_filter :session_expiration, :user_setup, :check_if_login_required, :set_localization
-
-
1
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
-
1
rescue_from ::Unauthorized, :with => :deny_access
-
1
rescue_from ::ActionView::MissingTemplate, :with => :missing_template
-
-
1
include Redmine::Search::Controller
-
1
include Redmine::MenuManager::MenuController
-
1
helper Redmine::MenuManager::MenuHelper
-
-
1
def session_expiration
-
841
if session[:user_id]
-
582
if session_expired? && !try_to_autologin
-
reset_session
-
flash[:error] = l(:error_session_expired)
-
redirect_to signin_url
-
else
-
582
session[:atime] = Time.now.utc.to_i
-
end
-
end
-
end
-
-
1
def session_expired?
-
582
if Setting.session_lifetime?
-
unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
-
return true
-
end
-
end
-
582
if Setting.session_timeout?
-
unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
-
return true
-
end
-
end
-
582
false
-
end
-
-
1
def start_user_session(user)
-
121
session[:user_id] = user.id
-
121
session[:ctime] = Time.now.utc.to_i
-
121
session[:atime] = Time.now.utc.to_i
-
end
-
-
1
def user_setup
-
# Check the settings cache for each request
-
841
Setting.check_cache
-
# Find the current user
-
841
User.current = find_current_user
-
841
logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
-
end
-
-
# Returns the current user or nil if no user is logged in
-
# and starts a session if needed
-
1
def find_current_user
-
841
user = nil
-
841
unless api_request?
-
839
if session[:user_id]
-
# existing session
-
582
user = (User.active.find(session[:user_id]) rescue nil)
-
elsif autologin_user = try_to_autologin
-
user = autologin_user
-
elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
-
# RSS key authentication does not start a session
-
user = User.find_by_rss_key(params[:key])
-
end
-
end
-
841
if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
-
2
if (key = api_key_from_request)
-
# Use API key
-
2
user = User.find_by_api_key(key)
-
else
-
# HTTP Basic, either username/password or API key/random
-
authenticate_with_http_basic do |username, password|
-
user = User.try_to_login(username, password) || User.find_by_api_key(username)
-
end
-
end
-
# Switch user if requested by an admin user
-
2
if user && user.admin? && (username = api_switch_user_from_request)
-
su = User.find_by_login(username)
-
if su && su.active?
-
logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
-
user = su
-
else
-
render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
-
end
-
end
-
end
-
841
user
-
end
-
-
1
def try_to_autologin
-
257
if cookies[:autologin] && Setting.autologin?
-
# auto-login feature starts a new session
-
user = User.try_to_autologin(cookies[:autologin])
-
if user
-
reset_session
-
start_user_session(user)
-
end
-
user
-
end
-
end
-
-
# Sets the logged in user
-
1
def logged_user=(user)
-
142
reset_session
-
142
if user && user.is_a?(User)
-
121
User.current = user
-
121
start_user_session(user)
-
else
-
21
User.current = User.anonymous
-
end
-
end
-
-
# Logs out current user
-
1
def logout_user
-
125
if User.current.logged?
-
21
cookies.delete :autologin
-
21
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
-
21
self.logged_user = nil
-
end
-
end
-
-
# check if login is globally required to access the application
-
1
def check_if_login_required
-
# no check needed if user is already logged in
-
595
return true if User.current.logged?
-
33
require_login if Setting.login_required?
-
end
-
-
1
def set_localization
-
841
lang = nil
-
841
if User.current.logged?
-
583
lang = find_language(User.current.language)
-
end
-
841
if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
-
47
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
-
47
if !accept_lang.blank?
-
47
accept_lang = accept_lang.downcase
-
47
lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
-
end
-
end
-
841
lang ||= Setting.default_language
-
841
set_language_if_valid(lang)
-
end
-
-
1
def require_login
-
130
if !User.current.logged?
-
# Extract only the basic url parameters on non-GET requests
-
6
if request.get?
-
1
url = url_for(params)
-
else
-
5
url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
-
end
-
6
respond_to do |format|
-
9
format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
-
6
format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
-
7
format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
-
8
format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
-
6
format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
-
end
-
6
return false
-
end
-
124
true
-
end
-
-
1
def require_admin
-
3
return unless require_login
-
3
if !User.current.admin?
-
render_403
-
return false
-
end
-
3
true
-
end
-
-
1
def deny_access
-
8
User.current.logged? ? render_403 : require_login
-
end
-
-
# Authorize the user for the requested action
-
1
def authorize(ctrl = params[:controller], action = params[:action], global = false)
-
373
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
-
373
if allowed
-
365
true
-
else
-
8
if @project && @project.archived?
-
render_403 :message => :notice_not_authorized_archived_project
-
else
-
8
deny_access
-
end
-
end
-
end
-
-
# Authorize the user for the requested action outside a project
-
1
def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
-
7
authorize(ctrl, action, global)
-
end
-
-
# Find project of id params[:id]
-
1
def find_project
-
69
@project = Project.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# Find project of id params[:project_id]
-
1
def find_project_by_project_id
-
@project = Project.find(params[:project_id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# Find a project based on params[:project_id]
-
# TODO: some subclasses override this, see about merging their logic
-
1
def find_optional_project
-
16
@project = Project.find(params[:project_id]) unless params[:project_id].blank?
-
16
allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
-
16
allowed ? true : deny_access
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# Finds and sets @project based on @object.project
-
1
def find_project_from_association
-
1
render_404 unless @object.present?
-
-
1
@project = @object.project
-
end
-
-
1
def find_model_object
-
1
model = self.class.model_object
-
1
if model
-
1
@object = model.find(params[:id])
-
1
self.instance_variable_set('@' + controller_name.singularize, @object) if @object
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def self.model_object(model)
-
6
self.model_object = model
-
end
-
-
# Find the issue whose id is the :id parameter
-
# Raises a Unauthorized exception if the issue is not visible
-
1
def find_issue
-
# Issue.visible.find(...) can not be used to redirect user to the login form
-
# if the issue actually exists but requires authentication
-
3
@issue = Issue.find(params[:id])
-
3
raise Unauthorized unless @issue.visible?
-
3
@project = @issue.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# Find issues with a single :id param or :ids array param
-
# Raises a Unauthorized exception if one of the issues is not visible
-
1
def find_issues
-
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
-
raise ActiveRecord::RecordNotFound if @issues.empty?
-
raise Unauthorized unless @issues.all?(&:visible?)
-
@projects = @issues.collect(&:project).compact.uniq
-
@project = @projects.first if @projects.size == 1
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# make sure that the user is a member of the project (or admin) if project is private
-
# used as a before_filter for actions that do not require any particular permission on the project
-
1
def check_project_privacy
-
if @project && !@project.archived?
-
if @project.visible?
-
true
-
else
-
deny_access
-
end
-
else
-
@project = nil
-
render_404
-
false
-
end
-
end
-
-
1
def back_url
-
url = params[:back_url]
-
if url.nil? && referer = request.env['HTTP_REFERER']
-
url = CGI.unescape(referer.to_s)
-
end
-
url
-
end
-
-
1
def redirect_back_or_default(default)
-
123
back_url = params[:back_url].to_s
-
123
if back_url.present?
-
2
begin
-
2
uri = URI.parse(back_url)
-
# do not redirect user to another host or to the login or register page
-
2
if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
-
2
redirect_to(back_url)
-
2
return
-
end
-
rescue URI::InvalidURIError
-
logger.warn("Could not redirect to invalid URL #{back_url}")
-
# redirect to default
-
end
-
end
-
121
redirect_to default
-
121
false
-
end
-
-
# Redirects to the request referer if present, redirects to args or call block otherwise.
-
1
def redirect_to_referer_or(*args, &block)
-
redirect_to :back
-
rescue ::ActionController::RedirectBackError
-
if args.any?
-
redirect_to *args
-
elsif block_given?
-
block.call
-
else
-
raise "#redirect_to_referer_or takes arguments or a block"
-
end
-
end
-
-
1
def render_403(options={})
-
2
@project = nil
-
2
render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
-
2
return false
-
end
-
-
1
def render_404(options={})
-
render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
-
return false
-
end
-
-
# Renders an error response
-
1
def render_error(arg)
-
2
arg = {:message => arg} unless arg.is_a?(Hash)
-
-
2
@message = arg[:message]
-
2
@message = l(@message) if @message.is_a?(Symbol)
-
2
@status = arg[:status] || 500
-
-
2
respond_to do |format|
-
2
format.html {
-
2
render :template => 'common/error', :layout => use_layout, :status => @status
-
}
-
2
format.any { head @status }
-
end
-
end
-
-
# Handler for ActionView::MissingTemplate exception
-
1
def missing_template
-
logger.warn "Missing template, responding with 404"
-
@project = nil
-
render_404
-
end
-
-
# Filter for actions that provide an API response
-
# but have no HTML representation for non admin users
-
1
def require_admin_or_api_request
-
1
return true if api_request?
-
if User.current.admin?
-
true
-
elsif User.current.logged?
-
render_error(:status => 406)
-
else
-
deny_access
-
end
-
end
-
-
# Picks which layout to use based on the request
-
#
-
# @return [boolean, string] name of the layout to use or false for no layout
-
1
def use_layout
-
2
request.xhr? ? false : 'base'
-
end
-
-
1
def invalid_authenticity_token
-
if api_request?
-
logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
-
end
-
render_error "Invalid form authenticity token."
-
end
-
-
1
def render_feed(items, options={})
-
@items = items || []
-
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
-
@items = @items.slice(0, Setting.feeds_limit.to_i)
-
@title = options[:title] || Setting.app_title
-
render :template => "common/feed", :formats => [:atom], :layout => false,
-
:content_type => 'application/atom+xml'
-
end
-
-
1
def self.accept_rss_auth(*actions)
-
8
if actions.any?
-
8
self.accept_rss_auth_actions = actions
-
else
-
self.accept_rss_auth_actions || []
-
end
-
end
-
-
1
def accept_rss_auth?(action=action_name)
-
self.class.accept_rss_auth.include?(action.to_sym)
-
end
-
-
1
def self.accept_api_auth(*actions)
-
22
if actions.any?
-
18
self.accept_api_auth_actions = actions
-
else
-
4
self.accept_api_auth_actions || []
-
end
-
end
-
-
1
def accept_api_auth?(action=action_name)
-
4
self.class.accept_api_auth.include?(action.to_sym)
-
end
-
-
# Returns the number of objects that should be displayed
-
# on the paginated list
-
1
def per_page_option
-
20
per_page = nil
-
20
if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
-
per_page = params[:per_page].to_s.to_i
-
session[:per_page] = per_page
-
elsif session[:per_page]
-
per_page = session[:per_page]
-
else
-
20
per_page = Setting.per_page_options_array.first || 25
-
end
-
20
per_page
-
end
-
-
# Returns offset and limit used to retrieve objects
-
# for an API response based on offset, limit and page parameters
-
1
def api_offset_and_limit(options=params)
-
if options[:offset].present?
-
offset = options[:offset].to_i
-
if offset < 0
-
offset = 0
-
end
-
end
-
limit = options[:limit].to_i
-
if limit < 1
-
limit = 25
-
elsif limit > 100
-
limit = 100
-
end
-
if offset.nil? && options[:page].present?
-
offset = (options[:page].to_i - 1) * limit
-
offset = 0 if offset < 0
-
end
-
offset ||= 0
-
-
[offset, limit]
-
end
-
-
# qvalues http header parser
-
# code taken from webrick
-
1
def parse_qvalues(value)
-
47
tmp = []
-
47
if value
-
47
parts = value.split(/,\s*/)
-
47
parts.each {|part|
-
94
if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
-
94
val = m[1]
-
94
q = (m[2] or 1).to_f
-
94
tmp.push([val, q])
-
end
-
}
-
141
tmp = tmp.sort_by{|val, q| -q}
-
141
tmp.collect!{|val, q| val}
-
end
-
47
return tmp
-
rescue
-
nil
-
end
-
-
# Returns a string that can be used as filename value in Content-Disposition header
-
1
def filename_for_content_disposition(name)
-
request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
-
end
-
-
1
def api_request?
-
2010
%w(xml json).include? params[:format]
-
end
-
-
# Returns the API key present in the request
-
1
def api_key_from_request
-
2
if params[:key].present?
-
2
params[:key].to_s
-
elsif request.headers["X-Redmine-API-Key"].present?
-
request.headers["X-Redmine-API-Key"].to_s
-
end
-
end
-
-
# Returns the API 'switch user' value if present
-
1
def api_switch_user_from_request
-
request.headers["X-Redmine-Switch-User"].to_s.presence
-
end
-
-
# Renders a warning flash if obj has unsaved attachments
-
1
def render_attachment_warning_if_needed(obj)
-
2
flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
-
end
-
-
# Sets the `flash` notice or error based the number of issues that did not save
-
#
-
# @param [Array, Issue] issues all of the saved and unsaved Issues
-
# @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
-
1
def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
-
if unsaved_issue_ids.empty?
-
flash[:notice] = l(:notice_successful_update) unless issues.empty?
-
else
-
flash[:error] = l(:notice_failed_to_save_issues,
-
:count => unsaved_issue_ids.size,
-
:total => issues.size,
-
:ids => '#' + unsaved_issue_ids.join(', #'))
-
end
-
end
-
-
# Rescues an invalid query statement. Just in case...
-
1
def query_statement_invalid(exception)
-
logger.error "Query::StatementInvalid: #{exception.message}" if logger
-
session.delete(:query)
-
sort_clear if respond_to?(:sort_clear)
-
render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
-
end
-
-
# Renders a 200 response for successfull updates or deletions via the API
-
1
def render_api_ok
-
render_api_head :ok
-
end
-
-
# Renders a head API response
-
1
def render_api_head(status)
-
# #head would return a response body with one space
-
render :text => '', :status => status, :layout => nil
-
end
-
-
# Renders API response on validation failure
-
1
def render_validation_errors(objects)
-
if objects.is_a?(Array)
-
@error_messages = objects.map {|object| object.errors.full_messages}.flatten
-
else
-
@error_messages = objects.errors.full_messages
-
end
-
render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
-
end
-
-
# Overrides #_include_layout? so that #render with no arguments
-
# doesn't use the layout for api requests
-
1
def _include_layout?(*args)
-
1152
api_request? ? false : super
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class AttachmentsController < ApplicationController
-
1
before_filter :find_project, :except => :upload
-
1
before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
-
1
before_filter :delete_authorize, :only => :destroy
-
1
before_filter :authorize_global, :only => :upload
-
-
1
accept_api_auth :show, :download, :upload
-
-
1
def show
-
respond_to do |format|
-
format.html {
-
if @attachment.is_diff?
-
@diff = File.new(@attachment.diskfile, "rb").read
-
@diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
-
@diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
-
# Save diff type as user preference
-
if User.current.logged? && @diff_type != User.current.pref[:diff_type]
-
User.current.pref[:diff_type] = @diff_type
-
User.current.preference.save
-
end
-
render :action => 'diff'
-
elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
-
@content = File.new(@attachment.diskfile, "rb").read
-
render :action => 'file'
-
else
-
download
-
end
-
}
-
format.api
-
end
-
end
-
-
1
def download
-
if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
-
@attachment.increment_download
-
end
-
-
if stale?(:etag => @attachment.digest)
-
# images are sent inline
-
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
-
:type => detect_content_type(@attachment),
-
:disposition => (@attachment.image? ? 'inline' : 'attachment')
-
end
-
end
-
-
1
def thumbnail
-
if @attachment.thumbnailable? && thumbnail = @attachment.thumbnail(:size => params[:size])
-
if stale?(:etag => thumbnail)
-
send_file thumbnail,
-
:filename => filename_for_content_disposition(@attachment.filename),
-
:type => detect_content_type(@attachment),
-
:disposition => 'inline'
-
end
-
else
-
# No thumbnail for the attachment or thumbnail could not be created
-
render :nothing => true, :status => 404
-
end
-
end
-
-
1
def upload
-
# Make sure that API users get used to set this content type
-
# as it won't trigger Rails' automatic parsing of the request body for parameters
-
unless request.content_type == 'application/octet-stream'
-
render :nothing => true, :status => 406
-
return
-
end
-
-
@attachment = Attachment.new(:file => request.raw_post)
-
@attachment.author = User.current
-
@attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
-
-
if @attachment.save
-
respond_to do |format|
-
format.api { render :action => 'upload', :status => :created }
-
end
-
else
-
respond_to do |format|
-
format.api { render_validation_errors(@attachment) }
-
end
-
end
-
end
-
-
1
def destroy
-
if @attachment.container.respond_to?(:init_journal)
-
@attachment.container.init_journal(User.current)
-
end
-
# Make sure association callbacks are called
-
@attachment.container.attachments.delete(@attachment)
-
redirect_to_referer_or project_path(@project)
-
end
-
-
1
private
-
1
def find_project
-
@attachment = Attachment.find(params[:id])
-
# Show 404 if the filename in the url is wrong
-
raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
-
@project = @attachment.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# Checks that the file exists and is readable
-
1
def file_readable
-
@attachment.readable? ? true : render_404
-
end
-
-
1
def read_authorize
-
@attachment.visible? ? true : deny_access
-
end
-
-
1
def delete_authorize
-
@attachment.deletable? ? true : deny_access
-
end
-
-
1
def detect_content_type(attachment)
-
content_type = attachment.content_type
-
if content_type.blank?
-
content_type = Redmine::MimeType.of(attachment.filename)
-
end
-
content_type.to_s
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class AuthSourcesController < ApplicationController
-
1
layout 'admin'
-
1
menu_item :ldap_authentication
-
-
1
before_filter :require_admin
-
-
1
def index
-
@auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 10
-
end
-
-
1
def new
-
klass_name = params[:type] || 'AuthSourceLdap'
-
@auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source])
-
end
-
-
1
def create
-
@auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source])
-
if @auth_source.save
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index'
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
@auth_source = AuthSource.find(params[:id])
-
end
-
-
1
def update
-
@auth_source = AuthSource.find(params[:id])
-
if @auth_source.update_attributes(params[:auth_source])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index'
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def test_connection
-
@auth_source = AuthSource.find(params[:id])
-
begin
-
@auth_source.test_connection
-
flash[:notice] = l(:notice_successful_connection)
-
rescue Exception => e
-
flash[:error] = l(:error_unable_to_connect, e.message)
-
end
-
redirect_to :action => 'index'
-
end
-
-
1
def destroy
-
@auth_source = AuthSource.find(params[:id])
-
unless @auth_source.users.find(:first)
-
@auth_source.destroy
-
flash[:notice] = l(:notice_successful_delete)
-
end
-
redirect_to :action => 'index'
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class AutoCompletesController < ApplicationController
-
1
before_filter :find_project
-
-
1
def issues
-
@issues = []
-
q = (params[:q] || params[:term]).to_s.strip
-
if q.present?
-
scope = (params[:scope] == "all" || @project.nil? ? Issue : @project.issues).visible
-
if q.match(/^\d+$/)
-
@issues << scope.find_by_id(q.to_i)
-
end
-
@issues += scope.where("LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%").order("#{Issue.table_name}.id DESC").limit(10).all
-
@issues.compact!
-
end
-
render :layout => false
-
end
-
-
1
private
-
-
1
def find_project
-
if params[:project_id].present?
-
@project = Project.find(params[:project_id])
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class BoardsController < ApplicationController
-
1
default_search_scope :messages
-
1
before_filter :find_project_by_project_id, :find_board_if_available, :authorize
-
1
accept_rss_auth :index, :show
-
-
1
helper :sort
-
1
include SortHelper
-
1
helper :watchers
-
-
1
def index
-
@boards = @project.boards.includes(:last_message => :author).all
-
# show the board if there is only one
-
if @boards.size == 1
-
@board = @boards.first
-
show
-
end
-
end
-
-
1
def show
-
respond_to do |format|
-
format.html {
-
sort_init 'updated_on', 'desc'
-
sort_update 'created_on' => "#{Message.table_name}.created_on",
-
'replies' => "#{Message.table_name}.replies_count",
-
'updated_on' => "#{Message.table_name}.updated_on"
-
-
@topic_count = @board.topics.count
-
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
-
@topics = @board.topics.reorder("#{Message.table_name}.sticky DESC").order(sort_clause).all(
-
:include => [:author, {:last_reply => :author}],
-
:limit => @topic_pages.items_per_page,
-
:offset => @topic_pages.current.offset)
-
@message = Message.new(:board => @board)
-
render :action => 'show', :layout => !request.xhr?
-
}
-
format.atom {
-
@messages = @board.messages.find :all, :order => 'created_on DESC',
-
:include => [:author, :board],
-
:limit => Setting.feeds_limit.to_i
-
render_feed(@messages, :title => "#{@project}: #{@board}")
-
}
-
end
-
end
-
-
1
def new
-
@board = @project.boards.build
-
@board.safe_attributes = params[:board]
-
end
-
-
1
def create
-
@board = @project.boards.build
-
@board.safe_attributes = params[:board]
-
if @board.save
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to_settings_in_projects
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@board.safe_attributes = params[:board]
-
if @board.save
-
redirect_to_settings_in_projects
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@board.destroy
-
redirect_to_settings_in_projects
-
end
-
-
1
private
-
1
def redirect_to_settings_in_projects
-
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
-
end
-
-
1
def find_board_if_available
-
@board = @project.boards.find(params[:id]) if params[:id]
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CalendarsController < ApplicationController
-
1
menu_item :calendar
-
1
before_filter :find_optional_project
-
-
1
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
-
-
1
helper :issues
-
1
helper :projects
-
1
helper :queries
-
1
include QueriesHelper
-
1
helper :sort
-
1
include SortHelper
-
-
1
def show
-
if params[:year] and params[:year].to_i > 1900
-
@year = params[:year].to_i
-
if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
-
@month = params[:month].to_i
-
end
-
end
-
@year ||= Date.today.year
-
@month ||= Date.today.month
-
-
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
-
retrieve_query
-
@query.group_by = nil
-
if @query.valid?
-
events = []
-
events += @query.issues(:include => [:tracker, :assigned_to, :priority],
-
:conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
-
)
-
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
-
-
@calendar.events = events
-
end
-
-
render :action => 'show', :layout => false if request.xhr?
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CommentsController < ApplicationController
-
1
default_search_scope :news
-
1
model_object News
-
1
before_filter :find_model_object
-
1
before_filter :find_project_from_association
-
1
before_filter :authorize
-
-
1
def create
-
raise Unauthorized unless @news.commentable?
-
-
@comment = Comment.new
-
@comment.safe_attributes = params[:comment]
-
@comment.author = User.current
-
if @news.comments << @comment
-
flash[:notice] = l(:label_comment_added)
-
end
-
-
redirect_to :controller => 'news', :action => 'show', :id => @news
-
end
-
-
1
def destroy
-
@news.comments.find(params[:comment_id]).destroy
-
redirect_to :controller => 'news', :action => 'show', :id => @news
-
end
-
-
1
private
-
-
# ApplicationController's find_model_object sets it based on the controller
-
# name so it needs to be overriden and set to @news instead
-
1
def find_model_object
-
super
-
@news = @object
-
@comment = nil
-
@news
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ContextMenusController < ApplicationController
-
1
helper :watchers
-
1
helper :issues
-
-
1
def issues
-
@issues = Issue.visible.all(:conditions => {:id => params[:ids]}, :include => :project)
-
if (@issues.size == 1)
-
@issue = @issues.first
-
end
-
@issue_ids = @issues.map(&:id).sort
-
-
@allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
-
@projects = @issues.collect(&:project).compact.uniq
-
@project = @projects.first if @projects.size == 1
-
-
@can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
-
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
-
:update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
-
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
-
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
-
:delete => User.current.allowed_to?(:delete_issues, @projects)
-
}
-
if @project
-
if @issue
-
@assignables = @issue.assignable_users
-
else
-
@assignables = @project.assignable_users
-
end
-
@trackers = @project.trackers
-
else
-
#when multiple projects, we only keep the intersection of each set
-
@assignables = @projects.map(&:assignable_users).reduce(:&)
-
@trackers = @projects.map(&:trackers).reduce(:&)
-
end
-
@versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
-
-
@priorities = IssuePriority.active.reverse
-
@back = back_url
-
-
@options_by_custom_field = {}
-
if @can[:edit]
-
custom_fields = @issues.map(&:available_custom_fields).reduce(:&).select do |f|
-
%w(bool list user version).include?(f.field_format) && !f.multiple?
-
end
-
custom_fields.each do |field|
-
values = field.possible_values_options(@projects)
-
if values.any?
-
@options_by_custom_field[field] = values
-
end
-
end
-
end
-
-
@safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
-
render :layout => false
-
end
-
-
1
def time_entries
-
@time_entries = TimeEntry.all(
-
:conditions => {:id => params[:ids]}, :include => :project)
-
@projects = @time_entries.collect(&:project).compact.uniq
-
@project = @projects.first if @projects.size == 1
-
@activities = TimeEntryActivity.shared.active
-
@can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
-
:delete => User.current.allowed_to?(:edit_time_entries, @projects)
-
}
-
@back = back_url
-
render :layout => false
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CustomFieldsController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin
-
1
before_filter :build_new_custom_field, :only => [:new, :create]
-
1
before_filter :find_custom_field, :only => [:edit, :update, :destroy]
-
-
1
def index
-
@custom_fields_by_type = CustomField.find(:all).group_by {|f| f.class.name }
-
@tab = params[:tab] || 'IssueCustomField'
-
end
-
-
1
def new
-
end
-
-
1
def create
-
if request.post? and @custom_field.save
-
flash[:notice] = l(:notice_successful_create)
-
call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
-
redirect_to :action => 'index', :tab => @custom_field.class.name
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
if request.put? and @custom_field.update_attributes(params[:custom_field])
-
flash[:notice] = l(:notice_successful_update)
-
call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
-
redirect_to :action => 'index', :tab => @custom_field.class.name
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@custom_field.destroy
-
redirect_to :action => 'index', :tab => @custom_field.class.name
-
rescue
-
flash[:error] = l(:error_can_not_delete_custom_field)
-
redirect_to :action => 'index'
-
end
-
-
1
private
-
-
1
def build_new_custom_field
-
@custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field])
-
if @custom_field.nil?
-
render_404
-
end
-
end
-
-
1
def find_custom_field
-
@custom_field = CustomField.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class DocumentsController < ApplicationController
-
1
default_search_scope :documents
-
1
model_object Document
-
1
before_filter :find_project_by_project_id, :only => [:index, :new, :create]
-
1
before_filter :find_model_object, :except => [:index, :new, :create]
-
1
before_filter :find_project_from_association, :except => [:index, :new, :create]
-
1
before_filter :authorize
-
-
1
helper :attachments
-
-
1
def index
-
@sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
-
documents = @project.documents.find :all, :include => [:attachments, :category]
-
case @sort_by
-
when 'date'
-
@grouped = documents.group_by {|d| d.updated_on.to_date }
-
when 'title'
-
@grouped = documents.group_by {|d| d.title.first.upcase}
-
when 'author'
-
@grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
-
else
-
@grouped = documents.group_by(&:category)
-
end
-
@document = @project.documents.build
-
render :layout => false if request.xhr?
-
end
-
-
1
def show
-
@attachments = @document.attachments.find(:all, :order => "created_on DESC")
-
end
-
-
1
def new
-
@document = @project.documents.build
-
@document.safe_attributes = params[:document]
-
end
-
-
1
def create
-
@document = @project.documents.build
-
@document.safe_attributes = params[:document]
-
@document.save_attachments(params[:attachments])
-
if @document.save
-
render_attachment_warning_if_needed(@document)
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index', :project_id => @project
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@document.safe_attributes = params[:document]
-
if request.put? and @document.save
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'show', :id => @document
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@document.destroy if request.delete?
-
redirect_to :controller => 'documents', :action => 'index', :project_id => @project
-
end
-
-
1
def add_attachment
-
attachments = Attachment.attach_files(@document, params[:attachments])
-
render_attachment_warning_if_needed(@document)
-
-
if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
-
Mailer.attachments_added(attachments[:files]).deliver
-
end
-
redirect_to :action => 'show', :id => @document
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class EnumerationsController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin, :except => :index
-
1
before_filter :require_admin_or_api_request, :only => :index
-
1
before_filter :build_new_enumeration, :only => [:new, :create]
-
1
before_filter :find_enumeration, :only => [:edit, :update, :destroy]
-
1
accept_api_auth :index
-
-
1
helper :custom_fields
-
-
1
def index
-
respond_to do |format|
-
format.html
-
format.api {
-
@klass = Enumeration.get_subclass(params[:type])
-
if @klass
-
@enumerations = @klass.shared.sorted.all
-
else
-
render_404
-
end
-
}
-
end
-
end
-
-
1
def new
-
end
-
-
1
def create
-
if request.post? && @enumeration.save
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index'
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
if request.put? && @enumeration.update_attributes(params[:enumeration])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index'
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
if !@enumeration.in_use?
-
# No associated objects
-
@enumeration.destroy
-
redirect_to :action => 'index'
-
return
-
elsif params[:reassign_to_id]
-
if reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id])
-
@enumeration.destroy(reassign_to)
-
redirect_to :action => 'index'
-
return
-
end
-
end
-
@enumerations = @enumeration.class.all - [@enumeration]
-
end
-
-
1
private
-
-
1
def build_new_enumeration
-
class_name = params[:enumeration] && params[:enumeration][:type] || params[:type]
-
@enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration])
-
if @enumeration.nil?
-
render_404
-
end
-
end
-
-
1
def find_enumeration
-
@enumeration = Enumeration.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class FilesController < ApplicationController
-
1
menu_item :files
-
-
1
before_filter :find_project_by_project_id
-
1
before_filter :authorize
-
-
1
helper :sort
-
1
include SortHelper
-
-
1
def index
-
sort_init 'filename', 'asc'
-
sort_update 'filename' => "#{Attachment.table_name}.filename",
-
'created_on' => "#{Attachment.table_name}.created_on",
-
'size' => "#{Attachment.table_name}.filesize",
-
'downloads' => "#{Attachment.table_name}.downloads"
-
-
@containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
-
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
-
render :layout => !request.xhr?
-
end
-
-
1
def new
-
@versions = @project.versions.sort
-
end
-
-
1
def create
-
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
-
attachments = Attachment.attach_files(container, params[:attachments])
-
render_attachment_warning_if_needed(container)
-
-
if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added')
-
Mailer.attachments_added(attachments[:files]).deliver
-
end
-
redirect_to project_files_path(@project)
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class GanttsController < ApplicationController
-
1
menu_item :gantt
-
1
before_filter :find_optional_project
-
-
1
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
-
-
1
helper :gantt
-
1
helper :issues
-
1
helper :projects
-
1
helper :queries
-
1
include QueriesHelper
-
1
helper :sort
-
1
include SortHelper
-
1
include Redmine::Export::PDF
-
-
1
def show
-
@gantt = Redmine::Helpers::Gantt.new(params)
-
@gantt.project = @project
-
retrieve_query
-
@query.group_by = nil
-
@gantt.query = @query if @query.valid?
-
-
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
-
-
respond_to do |format|
-
format.html { render :action => "show", :layout => !request.xhr? }
-
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
-
format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") }
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class GroupsController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin
-
1
before_filter :find_group, :except => [:index, :new, :create]
-
1
accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user
-
-
1
helper :custom_fields
-
-
1
def index
-
@groups = Group.sorted.all
-
-
respond_to do |format|
-
format.html
-
format.api
-
end
-
end
-
-
1
def show
-
respond_to do |format|
-
format.html
-
format.api
-
end
-
end
-
-
1
def new
-
@group = Group.new
-
end
-
-
1
def create
-
@group = Group.new
-
@group.safe_attributes = params[:group]
-
-
respond_to do |format|
-
if @group.save
-
format.html {
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to(params[:continue] ? new_group_path : groups_path)
-
}
-
format.api { render :action => 'show', :status => :created, :location => group_url(@group) }
-
else
-
format.html { render :action => "new" }
-
format.api { render_validation_errors(@group) }
-
end
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@group.safe_attributes = params[:group]
-
-
respond_to do |format|
-
if @group.save
-
flash[:notice] = l(:notice_successful_update)
-
format.html { redirect_to(groups_path) }
-
format.api { render_api_ok }
-
else
-
format.html { render :action => "edit" }
-
format.api { render_validation_errors(@group) }
-
end
-
end
-
end
-
-
1
def destroy
-
@group.destroy
-
-
respond_to do |format|
-
format.html { redirect_to(groups_url) }
-
format.api { render_api_ok }
-
end
-
end
-
-
1
def add_users
-
@users = User.find_all_by_id(params[:user_id] || params[:user_ids])
-
@group.users << @users if request.post?
-
respond_to do |format|
-
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
-
format.js
-
format.api { render_api_ok }
-
end
-
end
-
-
1
def remove_user
-
@group.users.delete(User.find(params[:user_id])) if request.delete?
-
respond_to do |format|
-
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
-
format.js
-
format.api { render_api_ok }
-
end
-
end
-
-
1
def autocomplete_for_user
-
@users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100)
-
render :layout => false
-
end
-
-
1
def edit_membership
-
@membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
-
@membership.save if request.post?
-
respond_to do |format|
-
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
-
format.js
-
end
-
end
-
-
1
def destroy_membership
-
Member.find(params[:membership_id]).destroy if request.post?
-
respond_to do |format|
-
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
-
format.js
-
end
-
end
-
-
1
private
-
-
1
def find_group
-
@group = Group.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueCategoriesController < ApplicationController
-
1
menu_item :settings
-
1
model_object IssueCategory
-
1
before_filter :find_model_object, :except => [:index, :new, :create]
-
1
before_filter :find_project_from_association, :except => [:index, :new, :create]
-
1
before_filter :find_project_by_project_id, :only => [:index, :new, :create]
-
1
before_filter :authorize
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
def index
-
respond_to do |format|
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project }
-
format.api { @categories = @project.issue_categories.all }
-
end
-
end
-
-
1
def show
-
respond_to do |format|
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project }
-
format.api
-
end
-
end
-
-
1
def new
-
@category = @project.issue_categories.build
-
@category.safe_attributes = params[:issue_category]
-
-
respond_to do |format|
-
format.html
-
format.js
-
end
-
end
-
-
1
def create
-
@category = @project.issue_categories.build
-
@category.safe_attributes = params[:issue_category]
-
if @category.save
-
respond_to do |format|
-
format.html do
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
-
end
-
format.js
-
format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'new'}
-
format.js { render :action => 'new'}
-
format.api { render_validation_errors(@category) }
-
end
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@category.safe_attributes = params[:issue_category]
-
if @category.save
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
-
}
-
format.api { render_api_ok }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'edit' }
-
format.api { render_validation_errors(@category) }
-
end
-
end
-
end
-
-
1
def destroy
-
@issue_count = @category.issues.size
-
if @issue_count == 0 || params[:todo] || api_request?
-
reassign_to = nil
-
if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?)
-
reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id])
-
end
-
@category.destroy(reassign_to)
-
respond_to do |format|
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' }
-
format.api { render_api_ok }
-
end
-
return
-
end
-
@categories = @project.issue_categories - [@category]
-
end
-
-
1
private
-
# Wrap ApplicationController's find_model_object method to set
-
# @category instead of just @issue_category
-
1
def find_model_object
-
super
-
@category = @object
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueRelationsController < ApplicationController
-
1
before_filter :find_issue, :find_project_from_association, :authorize, :only => [:index, :create]
-
1
before_filter :find_relation, :except => [:index, :create]
-
-
1
accept_api_auth :index, :show, :create, :destroy
-
-
1
def index
-
@relations = @issue.relations
-
-
respond_to do |format|
-
format.html { render :nothing => true }
-
format.api
-
end
-
end
-
-
1
def show
-
raise Unauthorized unless @relation.visible?
-
-
respond_to do |format|
-
format.html { render :nothing => true }
-
format.api
-
end
-
end
-
-
1
def create
-
@relation = IssueRelation.new(params[:relation])
-
@relation.issue_from = @issue
-
if params[:relation] && m = params[:relation][:issue_to_id].to_s.strip.match(/^#?(\d+)$/)
-
@relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
-
end
-
saved = @relation.save
-
-
respond_to do |format|
-
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
-
format.js {
-
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
-
}
-
format.api {
-
if saved
-
render :action => 'show', :status => :created, :location => relation_url(@relation)
-
else
-
render_validation_errors(@relation)
-
end
-
}
-
end
-
end
-
-
1
def destroy
-
raise Unauthorized unless @relation.deletable?
-
@relation.destroy
-
-
respond_to do |format|
-
format.html { redirect_to issue_path } # TODO : does this really work since @issue is always nil? What is it useful to?
-
format.js
-
format.api { render_api_ok }
-
end
-
end
-
-
1
private
-
1
def find_issue
-
@issue = @object = Issue.find(params[:issue_id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def find_relation
-
@relation = IssueRelation.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueStatusesController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin, :except => :index
-
1
before_filter :require_admin_or_api_request, :only => :index
-
1
accept_api_auth :index
-
-
1
def index
-
respond_to do |format|
-
format.html {
-
@issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 25, :order => "position"
-
render :action => "index", :layout => false if request.xhr?
-
}
-
format.api {
-
@issue_statuses = IssueStatus.all(:order => 'position')
-
}
-
end
-
end
-
-
1
def new
-
@issue_status = IssueStatus.new
-
end
-
-
1
def create
-
@issue_status = IssueStatus.new(params[:issue_status])
-
if request.post? && @issue_status.save
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index'
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
@issue_status = IssueStatus.find(params[:id])
-
end
-
-
1
def update
-
@issue_status = IssueStatus.find(params[:id])
-
if request.put? && @issue_status.update_attributes(params[:issue_status])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index'
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
IssueStatus.find(params[:id]).destroy
-
redirect_to :action => 'index'
-
rescue
-
flash[:error] = l(:error_unable_delete_issue_status)
-
redirect_to :action => 'index'
-
end
-
-
1
def update_issue_done_ratio
-
if request.post? && IssueStatus.update_issue_done_ratios
-
flash[:notice] = l(:notice_issue_done_ratios_updated)
-
else
-
flash[:error] = l(:error_issue_done_ratios_not_updated)
-
end
-
redirect_to :action => 'index'
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssuesController < ApplicationController
-
1
menu_item :new_issue, :only => [:new, :create]
-
1
default_search_scope :issues
-
-
1
before_filter :find_issue, :only => [:show, :edit, :update]
-
1
before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
-
1
before_filter :find_project, :only => [:new, :create]
-
1
before_filter :authorize, :except => [:index]
-
1
before_filter :find_optional_project, :only => [:index]
-
1
before_filter :check_for_default_issue_status, :only => [:new, :create]
-
1
before_filter :build_new_issue_from_params, :only => [:new, :create]
-
1
accept_rss_auth :index, :show
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
-
-
1
helper :journals
-
1
helper :projects
-
1
include ProjectsHelper
-
1
helper :custom_fields
-
1
include CustomFieldsHelper
-
1
helper :issue_relations
-
1
include IssueRelationsHelper
-
1
helper :watchers
-
1
include WatchersHelper
-
1
helper :attachments
-
1
include AttachmentsHelper
-
1
helper :queries
-
1
include QueriesHelper
-
1
helper :repositories
-
1
include RepositoriesHelper
-
1
helper :sort
-
1
include SortHelper
-
1
include IssuesHelper
-
1
helper :timelog
-
1
include Redmine::Export::PDF
-
-
1
def index
-
16
retrieve_query
-
16
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
-
16
sort_update(@query.sortable_columns)
-
16
@query.sort_criteria = sort_criteria.to_a
-
-
16
if @query.valid?
-
16
case params[:format]
-
when 'csv', 'pdf'
-
@limit = Setting.issues_export_limit.to_i
-
when 'atom'
-
@limit = Setting.feeds_limit.to_i
-
when 'xml', 'json'
-
@offset, @limit = api_offset_and_limit
-
else
-
16
@limit = per_page_option
-
end
-
-
16
@issue_count = @query.issue_count
-
16
@issue_pages = Paginator.new self, @issue_count, @limit, params['page']
-
16
@offset ||= @issue_pages.current.offset
-
16
@issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
-
:order => sort_clause,
-
:offset => @offset,
-
:limit => @limit)
-
16
@issue_count_by_group = @query.issue_count_by_group
-
-
16
respond_to do |format|
-
32
format.html { render :template => 'issues/index', :layout => !request.xhr? }
-
16
format.api {
-
Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
-
}
-
16
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
-
16
format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
-
16
format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
-
end
-
else
-
respond_to do |format|
-
format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
-
format.any(:atom, :csv, :pdf) { render(:nothing => true) }
-
format.api { render_validation_errors(@query) }
-
end
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def show
-
3
@journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
-
3
@journals.each_with_index {|j,i| j.indice = i+1}
-
3
@journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
-
3
@journals.reverse! if User.current.wants_comments_in_reverse_order?
-
-
3
@changesets = @issue.changesets.visible.all
-
3
@changesets.reverse! if User.current.wants_comments_in_reverse_order?
-
-
5
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
-
3
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
-
3
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
-
3
@priorities = IssuePriority.active
-
3
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
-
3
respond_to do |format|
-
3
format.html {
-
3
retrieve_previous_and_next_issue_ids
-
3
render :template => 'issues/show'
-
}
-
3
format.api
-
3
format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
-
3
format.pdf {
-
pdf = issue_to_pdf(@issue, :journals => @journals)
-
send_data(pdf, :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf")
-
}
-
end
-
end
-
-
# Add a new issue
-
# The new issue will be created from an existing one if copy_from parameter is given
-
1
def new
-
2
respond_to do |format|
-
4
format.html { render :action => 'new', :layout => !request.xhr? }
-
2
format.js { render :partial => 'update_form' }
-
end
-
end
-
-
1
def create
-
2
call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
-
2
@issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
-
2
if @issue.save
-
2
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
-
2
respond_to do |format|
-
2
format.html {
-
2
render_attachment_warning_if_needed(@issue)
-
2
flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
-
redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
-
2
{ :action => 'show', :id => @issue })
-
}
-
2
format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
-
end
-
return
-
else
-
respond_to do |format|
-
format.html { render :action => 'new' }
-
format.api { render_validation_errors(@issue) }
-
end
-
end
-
end
-
-
1
def edit
-
return unless update_issue_from_params
-
-
respond_to do |format|
-
format.html { }
-
format.xml { }
-
end
-
end
-
-
1
def update
-
return unless update_issue_from_params
-
@issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
-
saved = false
-
begin
-
saved = @issue.save_issue_with_child_records(params, @time_entry)
-
rescue ActiveRecord::StaleObjectError
-
@conflict = true
-
if params[:last_journal_id]
-
@conflict_journals = @issue.journals_after(params[:last_journal_id]).all
-
@conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
-
end
-
end
-
-
if saved
-
render_attachment_warning_if_needed(@issue)
-
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
-
-
respond_to do |format|
-
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
-
format.api { render_api_ok }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'edit' }
-
format.api { render_validation_errors(@issue) }
-
end
-
end
-
end
-
-
# Bulk edit/copy a set of issues
-
1
def bulk_edit
-
@issues.sort!
-
@copy = params[:copy].present?
-
@notes = params[:notes]
-
-
if User.current.allowed_to?(:move_issues, @projects)
-
@allowed_projects = Issue.allowed_target_projects_on_move
-
if params[:issue]
-
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
-
if @target_project
-
target_projects = [@target_project]
-
end
-
end
-
end
-
target_projects ||= @projects
-
-
if @copy
-
@available_statuses = [IssueStatus.default]
-
else
-
@available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
-
end
-
@custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
-
@assignables = target_projects.map(&:assignable_users).reduce(:&)
-
@trackers = target_projects.map(&:trackers).reduce(:&)
-
@versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
-
@categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
-
if @copy
-
@attachments_present = @issues.detect {|i| i.attachments.any?}.present?
-
@subtasks_present = @issues.detect {|i| !i.leaf?}.present?
-
end
-
-
@safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
-
render :layout => false if request.xhr?
-
end
-
-
1
def bulk_update
-
@issues.sort!
-
@copy = params[:copy].present?
-
attributes = parse_params_for_bulk_issue_attributes(params)
-
-
unsaved_issue_ids = []
-
moved_issues = []
-
-
if @copy && params[:copy_subtasks].present?
-
# Descendant issues will be copied with the parent task
-
# Don't copy them twice
-
@issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
-
end
-
-
@issues.each do |issue|
-
issue.reload
-
if @copy
-
issue = issue.copy({},
-
:attachments => params[:copy_attachments].present?,
-
:subtasks => params[:copy_subtasks].present?
-
)
-
end
-
journal = issue.init_journal(User.current, params[:notes])
-
issue.safe_attributes = attributes
-
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
-
if issue.save
-
moved_issues << issue
-
else
-
# Keep unsaved issue ids to display them in flash error
-
unsaved_issue_ids << issue.id
-
end
-
end
-
set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
-
-
if params[:follow]
-
if @issues.size == 1 && moved_issues.size == 1
-
redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
-
elsif moved_issues.map(&:project).uniq.size == 1
-
redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
-
end
-
else
-
redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
-
end
-
end
-
-
1
def destroy
-
@hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
-
if @hours > 0
-
case params[:todo]
-
when 'destroy'
-
# nothing to do
-
when 'nullify'
-
TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
-
when 'reassign'
-
reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
-
if reassign_to.nil?
-
flash.now[:error] = l(:error_issue_not_found_in_project)
-
return
-
else
-
TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
-
end
-
else
-
# display the destroy form if it's a user request
-
return unless api_request?
-
end
-
end
-
@issues.each do |issue|
-
begin
-
issue.reload.destroy
-
rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
-
# nothing to do, issue was already deleted (eg. by a parent)
-
end
-
end
-
respond_to do |format|
-
format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
-
format.api { render_api_ok }
-
end
-
end
-
-
1
private
-
-
1
def find_project
-
4
project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
-
4
@project = Project.find(project_id)
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def retrieve_previous_and_next_issue_ids
-
3
retrieve_query_from_session
-
3
if @query
-
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
-
sort_update(@query.sortable_columns, 'issues_index_sort')
-
limit = 500
-
issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
-
if (idx = issue_ids.index(@issue.id)) && idx < limit
-
if issue_ids.size < 500
-
@issue_position = idx + 1
-
@issue_count = issue_ids.size
-
end
-
@prev_issue_id = issue_ids[idx - 1] if idx > 0
-
@next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
-
end
-
end
-
end
-
-
# Used by #edit and #update to set some common instance variables
-
# from the params
-
# TODO: Refactor, not everything in here is needed by #edit
-
1
def update_issue_from_params
-
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
-
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
-
@time_entry.attributes = params[:time_entry]
-
-
@issue.init_journal(User.current)
-
-
issue_attributes = params[:issue]
-
if issue_attributes && params[:conflict_resolution]
-
case params[:conflict_resolution]
-
when 'overwrite'
-
issue_attributes = issue_attributes.dup
-
issue_attributes.delete(:lock_version)
-
when 'add_notes'
-
issue_attributes = issue_attributes.slice(:notes)
-
when 'cancel'
-
redirect_to issue_path(@issue)
-
return false
-
end
-
end
-
@issue.safe_attributes = issue_attributes
-
@priorities = IssuePriority.active
-
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
-
true
-
end
-
-
# TODO: Refactor, lots of extra code in here
-
# TODO: Changing tracker on an existing issue should not trigger this
-
1
def build_new_issue_from_params
-
4
if params[:id].blank?
-
4
@issue = Issue.new
-
4
if params[:copy_from]
-
4
begin
-
4
@copy_from = Issue.visible.find(params[:copy_from])
-
4
@copy_attachments = params[:copy_attachments].present? || request.get?
-
4
@copy_subtasks = params[:copy_subtasks].present? || request.get?
-
4
@issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks)
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
return
-
end
-
end
-
4
@issue.project = @project
-
else
-
@issue = @project.issues.visible.find(params[:id])
-
end
-
-
4
@issue.project = @project
-
4
@issue.author ||= User.current
-
# Tracker must be set before custom field values
-
4
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
-
4
if @issue.tracker.nil?
-
render_error l(:error_no_tracker_in_project)
-
return false
-
end
-
4
@issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
-
4
@issue.safe_attributes = params[:issue]
-
-
4
@priorities = IssuePriority.active
-
4
@allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
-
4
@available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
-
end
-
-
1
def check_for_default_issue_status
-
4
if IssueStatus.default.nil?
-
render_error l(:error_no_default_issue_status)
-
return false
-
end
-
end
-
-
1
def parse_params_for_bulk_issue_attributes(params)
-
attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
-
attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
-
if custom = attributes[:custom_field_values]
-
custom.reject! {|k,v| v.blank?}
-
custom.keys.each do |k|
-
if custom[k].is_a?(Array)
-
custom[k] << '' if custom[k].delete('__none__')
-
else
-
custom[k] = '' if custom[k] == '__none__'
-
end
-
end
-
end
-
attributes
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class JournalsController < ApplicationController
-
1
before_filter :find_journal, :only => [:edit, :diff]
-
1
before_filter :find_issue, :only => [:new]
-
1
before_filter :find_optional_project, :only => [:index]
-
1
before_filter :authorize, :only => [:new, :edit, :diff]
-
1
accept_rss_auth :index
-
1
menu_item :issues
-
-
1
helper :issues
-
1
helper :custom_fields
-
1
helper :queries
-
1
include QueriesHelper
-
1
helper :sort
-
1
include SortHelper
-
-
1
def index
-
retrieve_query
-
sort_init 'id', 'desc'
-
sort_update(@query.sortable_columns)
-
-
if @query.valid?
-
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
-
:limit => 25)
-
end
-
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
-
render :layout => false, :content_type => 'application/atom+xml'
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def diff
-
@issue = @journal.issue
-
if params[:detail_id].present?
-
@detail = @journal.details.find_by_id(params[:detail_id])
-
else
-
@detail = @journal.details.detect {|d| d.prop_key == 'description'}
-
end
-
(render_404; return false) unless @issue && @detail
-
@diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value)
-
end
-
-
1
def new
-
@journal = Journal.visible.find(params[:journal_id]) if params[:journal_id]
-
if @journal
-
user = @journal.user
-
text = @journal.notes
-
else
-
user = @issue.author
-
text = @issue.description
-
end
-
# Replaces pre blocks with [...]
-
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
-
@content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
-
@content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def edit
-
(render_403; return false) unless @journal.editable_by?(User.current)
-
if request.post?
-
@journal.update_attributes(:notes => params[:notes]) if params[:notes]
-
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
-
call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
-
respond_to do |format|
-
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
-
format.js { render :action => 'update' }
-
end
-
else
-
respond_to do |format|
-
format.html {
-
# TODO: implement non-JS journal update
-
render :nothing => true
-
}
-
format.js
-
end
-
end
-
end
-
-
1
private
-
-
1
def find_journal
-
@journal = Journal.visible.find(params[:id])
-
@project = @journal.journalized.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MailHandlerController < ActionController::Base
-
1
before_filter :check_credential
-
-
# Submits an incoming email to MailHandler
-
1
def index
-
options = params.dup
-
email = options.delete(:email)
-
if MailHandler.receive(email, options)
-
render :nothing => true, :status => :created
-
else
-
render :nothing => true, :status => :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def check_credential
-
User.current = nil
-
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
-
render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MembersController < ApplicationController
-
1
model_object Member
-
1
before_filter :find_model_object, :except => [:index, :create, :autocomplete]
-
1
before_filter :find_project_from_association, :except => [:index, :create, :autocomplete]
-
1
before_filter :find_project_by_project_id, :only => [:index, :create, :autocomplete]
-
1
before_filter :authorize
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
def index
-
@offset, @limit = api_offset_and_limit
-
@member_count = @project.member_principals.count
-
@member_pages = Paginator.new self, @member_count, @limit, params['page']
-
@offset ||= @member_pages.current.offset
-
@members = @project.member_principals.all(
-
:order => "#{Member.table_name}.id",
-
:limit => @limit,
-
:offset => @offset
-
)
-
-
respond_to do |format|
-
format.html { head 406 }
-
format.api
-
end
-
end
-
-
1
def show
-
respond_to do |format|
-
format.html { head 406 }
-
format.api
-
end
-
end
-
-
1
def create
-
members = []
-
if params[:membership]
-
if params[:membership][:user_ids]
-
attrs = params[:membership].dup
-
user_ids = attrs.delete(:user_ids)
-
user_ids.each do |user_id|
-
members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => user_id)
-
end
-
else
-
members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => params[:membership][:user_id])
-
end
-
@project.members << members
-
end
-
-
respond_to do |format|
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
-
format.js { @members = members }
-
format.api {
-
@member = members.first
-
if @member.valid?
-
render :action => 'show', :status => :created, :location => membership_url(@member)
-
else
-
render_validation_errors(@member)
-
end
-
}
-
end
-
end
-
-
1
def update
-
if params[:membership]
-
@member.role_ids = params[:membership][:role_ids]
-
end
-
saved = @member.save
-
respond_to do |format|
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
-
format.js
-
format.api {
-
if saved
-
render_api_ok
-
else
-
render_validation_errors(@member)
-
end
-
}
-
end
-
end
-
-
1
def destroy
-
if request.delete? && @member.deletable?
-
@member.destroy
-
end
-
respond_to do |format|
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
-
format.js
-
format.api {
-
if @member.destroyed?
-
render_api_ok
-
else
-
head :unprocessable_entity
-
end
-
}
-
end
-
end
-
-
1
def autocomplete
-
@principals = Principal.active.not_member_of(@project).like(params[:q]).all(:limit => 100)
-
render :layout => false
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MessagesController < ApplicationController
-
1
menu_item :boards
-
1
default_search_scope :messages
-
1
before_filter :find_board, :only => [:new, :preview]
-
1
before_filter :find_message, :except => [:new, :preview]
-
1
before_filter :authorize, :except => [:preview, :edit, :destroy]
-
-
1
helper :boards
-
1
helper :watchers
-
1
helper :attachments
-
1
include AttachmentsHelper
-
-
1
REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
-
-
# Show a topic and its replies
-
1
def show
-
page = params[:page]
-
# Find the page of the requested reply
-
if params[:r] && page.nil?
-
offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
-
page = 1 + offset / REPLIES_PER_PAGE
-
end
-
-
@reply_count = @topic.children.count
-
@reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page
-
@replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}],
-
:order => "#{Message.table_name}.created_on ASC",
-
:limit => @reply_pages.items_per_page,
-
:offset => @reply_pages.current.offset)
-
-
@reply = Message.new(:subject => "RE: #{@message.subject}")
-
render :action => "show", :layout => false if request.xhr?
-
end
-
-
# Create a new topic
-
1
def new
-
@message = Message.new
-
@message.author = User.current
-
@message.board = @board
-
@message.safe_attributes = params[:message]
-
if request.post?
-
@message.save_attachments(params[:attachments])
-
if @message.save
-
call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
-
render_attachment_warning_if_needed(@message)
-
redirect_to board_message_path(@board, @message)
-
end
-
end
-
end
-
-
# Reply to a topic
-
1
def reply
-
@reply = Message.new
-
@reply.author = User.current
-
@reply.board = @board
-
@reply.safe_attributes = params[:reply]
-
@topic.children << @reply
-
if !@reply.new_record?
-
call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
-
attachments = Attachment.attach_files(@reply, params[:attachments])
-
render_attachment_warning_if_needed(@reply)
-
end
-
redirect_to board_message_path(@board, @topic, :r => @reply)
-
end
-
-
# Edit a message
-
1
def edit
-
(render_403; return false) unless @message.editable_by?(User.current)
-
@message.safe_attributes = params[:message]
-
if request.post? && @message.save
-
attachments = Attachment.attach_files(@message, params[:attachments])
-
render_attachment_warning_if_needed(@message)
-
flash[:notice] = l(:notice_successful_update)
-
@message.reload
-
redirect_to board_message_path(@message.board, @message.root, :r => (@message.parent_id && @message.id))
-
end
-
end
-
-
# Delete a messages
-
1
def destroy
-
(render_403; return false) unless @message.destroyable_by?(User.current)
-
r = @message.to_param
-
@message.destroy
-
if @message.parent
-
redirect_to board_message_path(@board, @message.parent, :r => r)
-
else
-
redirect_to project_board_path(@project, @board)
-
end
-
end
-
-
1
def quote
-
@subject = @message.subject
-
@subject = "RE: #{@subject}" unless @subject.starts_with?('RE:')
-
-
@content = "#{ll(Setting.default_language, :text_user_wrote, @message.author)}\n> "
-
@content << @message.content.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
-
end
-
-
1
def preview
-
message = @board.messages.find_by_id(params[:id])
-
@attachements = message.attachments if message
-
@text = (params[:message] || params[:reply])[:content]
-
@previewed = message
-
render :partial => 'common/preview'
-
end
-
-
1
private
-
1
def find_message
-
find_board
-
@message = @board.messages.find(params[:id], :include => :parent)
-
@topic = @message.root
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def find_board
-
@board = Board.find(params[:board_id], :include => :project)
-
@project = @board.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MyController < ApplicationController
-
1
before_filter :require_login
-
-
1
helper :issues
-
1
helper :users
-
1
helper :custom_fields
-
-
1
BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
-
'issuesreportedbyme' => :label_reported_issues,
-
'issueswatched' => :label_watched_issues,
-
'news' => :label_news_latest,
-
'calendar' => :label_calendar,
-
'documents' => :label_document_plural,
-
'timelog' => :label_spent_time
-
}.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
-
-
1
DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
-
'right' => ['issuesreportedbyme']
-
}.freeze
-
-
1
def index
-
page
-
render :action => 'page'
-
end
-
-
# Show user's page
-
1
def page
-
121
@user = User.current
-
121
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
-
end
-
-
# Edit user's account
-
1
def account
-
@user = User.current
-
@pref = @user.pref
-
if request.post?
-
@user.safe_attributes = params[:user]
-
@user.pref.attributes = params[:pref]
-
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
-
if @user.save
-
@user.pref.save
-
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
-
set_language_if_valid @user.language
-
flash[:notice] = l(:notice_account_updated)
-
redirect_to :action => 'account'
-
return
-
end
-
end
-
end
-
-
# Destroys user's account
-
1
def destroy
-
@user = User.current
-
unless @user.own_account_deletable?
-
redirect_to :action => 'account'
-
return
-
end
-
-
if request.post? && params[:confirm]
-
@user.destroy
-
if @user.destroyed?
-
logout_user
-
flash[:notice] = l(:notice_account_deleted)
-
end
-
redirect_to home_path
-
end
-
end
-
-
# Manage user's password
-
1
def password
-
@user = User.current
-
unless @user.change_password_allowed?
-
flash[:error] = l(:notice_can_t_change_password)
-
redirect_to :action => 'account'
-
return
-
end
-
if request.post?
-
if @user.check_password?(params[:password])
-
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
-
if @user.save
-
flash[:notice] = l(:notice_account_password_updated)
-
redirect_to :action => 'account'
-
end
-
else
-
flash[:error] = l(:notice_account_wrong_password)
-
end
-
end
-
end
-
-
# Create a new feeds key
-
1
def reset_rss_key
-
if request.post?
-
if User.current.rss_token
-
User.current.rss_token.destroy
-
User.current.reload
-
end
-
User.current.rss_key
-
flash[:notice] = l(:notice_feeds_access_key_reseted)
-
end
-
redirect_to :action => 'account'
-
end
-
-
# Create a new API key
-
1
def reset_api_key
-
if request.post?
-
if User.current.api_token
-
User.current.api_token.destroy
-
User.current.reload
-
end
-
User.current.api_key
-
flash[:notice] = l(:notice_api_access_key_reseted)
-
end
-
redirect_to :action => 'account'
-
end
-
-
# User's page layout configuration
-
1
def page_layout
-
@user = User.current
-
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
-
@block_options = []
-
BLOCKS.each do |k, v|
-
unless %w(top left right).detect {|f| (@blocks[f] ||= []).include?(k)}
-
@block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
-
end
-
end
-
end
-
-
# Add a block to user's page
-
# The block is added on top of the page
-
# params[:block] : id of the block to add
-
1
def add_block
-
block = params[:block].to_s.underscore
-
if block.present? && BLOCKS.key?(block)
-
@user = User.current
-
layout = @user.pref[:my_page_layout] || {}
-
# remove if already present in a group
-
%w(top left right).each {|f| (layout[f] ||= []).delete block }
-
# add it on top
-
layout['top'].unshift block
-
@user.pref[:my_page_layout] = layout
-
@user.pref.save
-
end
-
redirect_to :action => 'page_layout'
-
end
-
-
# Remove a block to user's page
-
# params[:block] : id of the block to remove
-
1
def remove_block
-
block = params[:block].to_s.underscore
-
@user = User.current
-
# remove block in all groups
-
layout = @user.pref[:my_page_layout] || {}
-
%w(top left right).each {|f| (layout[f] ||= []).delete block }
-
@user.pref[:my_page_layout] = layout
-
@user.pref.save
-
redirect_to :action => 'page_layout'
-
end
-
-
# Change blocks order on user's page
-
# params[:group] : group to order (top, left or right)
-
# params[:list-(top|left|right)] : array of block ids of the group
-
1
def order_blocks
-
group = params[:group]
-
@user = User.current
-
if group.is_a?(String)
-
group_items = (params["blocks"] || []).collect(&:underscore)
-
group_items.each {|s| s.sub!(/^block_/, '')}
-
if group_items and group_items.is_a? Array
-
layout = @user.pref[:my_page_layout] || {}
-
# remove group blocks if they are presents in other groups
-
%w(top left right).each {|f|
-
layout[f] = (layout[f] || []) - group_items
-
}
-
layout[group] = group_items
-
@user.pref[:my_page_layout] = layout
-
@user.pref.save
-
end
-
end
-
render :nothing => true
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class NewsController < ApplicationController
-
1
default_search_scope :news
-
1
model_object News
-
1
before_filter :find_model_object, :except => [:new, :create, :index]
-
1
before_filter :find_project_from_association, :except => [:new, :create, :index]
-
1
before_filter :find_project_by_project_id, :only => [:new, :create]
-
1
before_filter :authorize, :except => [:index]
-
1
before_filter :find_optional_project, :only => :index
-
1
accept_rss_auth :index
-
1
accept_api_auth :index
-
-
1
helper :watchers
-
1
helper :attachments
-
-
1
def index
-
case params[:format]
-
when 'xml', 'json'
-
@offset, @limit = api_offset_and_limit
-
else
-
@limit = 10
-
end
-
-
scope = @project ? @project.news.visible : News.visible
-
-
@news_count = scope.count
-
@news_pages = Paginator.new self, @news_count, @limit, params['page']
-
@offset ||= @news_pages.current.offset
-
@newss = scope.all(:include => [:author, :project],
-
:order => "#{News.table_name}.created_on DESC",
-
:offset => @offset,
-
:limit => @limit)
-
-
respond_to do |format|
-
format.html {
-
@news = News.new # for adding news inline
-
render :layout => false if request.xhr?
-
}
-
format.api
-
format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
-
end
-
end
-
-
1
def show
-
@comments = @news.comments
-
@comments.reverse! if User.current.wants_comments_in_reverse_order?
-
end
-
-
1
def new
-
@news = News.new(:project => @project, :author => User.current)
-
end
-
-
1
def create
-
@news = News.new(:project => @project, :author => User.current)
-
@news.safe_attributes = params[:news]
-
@news.save_attachments(params[:attachments])
-
if @news.save
-
render_attachment_warning_if_needed(@news)
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :controller => 'news', :action => 'index', :project_id => @project
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@news.safe_attributes = params[:news]
-
@news.save_attachments(params[:attachments])
-
if @news.save
-
render_attachment_warning_if_needed(@news)
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'show', :id => @news
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@news.destroy
-
redirect_to :action => 'index', :project_id => @project
-
end
-
-
1
private
-
-
1
def find_optional_project
-
return true unless params[:project_id]
-
@project = Project.find(params[:project_id])
-
authorize
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class PreviewsController < ApplicationController
-
1
before_filter :find_project
-
-
1
def issue
-
@issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
-
if @issue
-
@attachements = @issue.attachments
-
@description = params[:issue] && params[:issue][:description]
-
if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
-
@description = nil
-
end
-
# params[:notes] is useful for preview of notes in issue history
-
@notes = params[:notes] || (params[:issue] ? params[:issue][:notes] : nil)
-
else
-
@description = (params[:issue] ? params[:issue][:description] : nil)
-
end
-
render :layout => false
-
end
-
-
1
def news
-
if params[:id].present? && news = News.visible.find_by_id(params[:id])
-
@previewed = news
-
@attachments = news.attachments
-
end
-
@text = (params[:news] ? params[:news][:description] : nil)
-
render :partial => 'common/preview'
-
end
-
-
1
private
-
-
1
def find_project
-
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
-
@project = Project.find(project_id)
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ProjectEnumerationsController < ApplicationController
-
1
before_filter :find_project_by_project_id
-
1
before_filter :authorize
-
-
1
def update
-
if request.put? && params[:enumerations]
-
Project.transaction do
-
params[:enumerations].each do |id, activity|
-
@project.update_or_create_time_entry_activity(id, activity)
-
end
-
end
-
flash[:notice] = l(:notice_successful_update)
-
end
-
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
-
end
-
-
1
def destroy
-
@project.time_entry_activities.each do |time_entry_activity|
-
time_entry_activity.destroy(time_entry_activity.parent)
-
end
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
-
end
-
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ProjectsController < ApplicationController
-
1
menu_item :overview
-
1
menu_item :roadmap, :only => :roadmap
-
1
menu_item :settings, :only => :settings
-
-
1
before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
-
1
before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
-
1
before_filter :authorize_global, :only => [:new, :create]
-
1
before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
-
1
accept_rss_auth :index
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
-
if controller.request.post?
-
controller.send :expire_action, :controller => 'welcome', :action => 'robots'
-
end
-
end
-
-
1
helper :sort
-
1
include SortHelper
-
1
helper :custom_fields
-
1
include CustomFieldsHelper
-
1
helper :issues
-
1
helper :queries
-
1
include QueriesHelper
-
1
helper :repositories
-
1
include RepositoriesHelper
-
1
include ProjectsHelper
-
-
# Lists visible projects
-
1
def index
-
respond_to do |format|
-
format.html {
-
scope = Project
-
unless params[:closed]
-
scope = scope.active
-
end
-
@projects = scope.visible.order('lft').all
-
}
-
format.api {
-
@offset, @limit = api_offset_and_limit
-
@project_count = Project.visible.count
-
@projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
-
}
-
format.atom {
-
projects = Project.visible.find(:all, :order => 'created_on DESC',
-
:limit => Setting.feeds_limit.to_i)
-
render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
-
}
-
end
-
end
-
-
1
def new
-
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
-
@trackers = Tracker.sorted.all
-
@project = Project.new
-
@project.safe_attributes = params[:project]
-
end
-
-
1
def create
-
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
-
@trackers = Tracker.sorted.all
-
@project = Project.new
-
@project.safe_attributes = params[:project]
-
-
if validate_parent_id && @project.save
-
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
-
# Add current user as a project member if he is not admin
-
unless User.current.admin?
-
r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
-
m = Member.new(:user => User.current, :roles => [r])
-
@project.members << m
-
end
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to(params[:continue] ?
-
{:controller => 'projects', :action => 'new', :project => {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}} :
-
{:controller => 'projects', :action => 'settings', :id => @project}
-
)
-
}
-
format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'new' }
-
format.api { render_validation_errors(@project) }
-
end
-
end
-
-
end
-
-
1
def copy
-
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
-
@trackers = Tracker.sorted.all
-
@root_projects = Project.find(:all,
-
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
-
:order => 'name')
-
@source_project = Project.find(params[:id])
-
if request.get?
-
@project = Project.copy_from(@source_project)
-
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
-
else
-
Mailer.with_deliveries(params[:notifications] == '1') do
-
@project = Project.new
-
@project.safe_attributes = params[:project]
-
if validate_parent_id && @project.copy(@source_project, :only => params[:only])
-
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :controller => 'projects', :action => 'settings', :id => @project
-
elsif !@project.new_record?
-
# Project was created
-
# But some objects were not copied due to validation failures
-
# (eg. issues from disabled trackers)
-
# TODO: inform about that
-
redirect_to :controller => 'projects', :action => 'settings', :id => @project
-
end
-
end
-
end
-
rescue ActiveRecord::RecordNotFound
-
# source_project not found
-
render_404
-
end
-
-
# Show @project
-
1
def show
-
62
if params[:jump]
-
# try to redirect to the requested menu item
-
redirect_to_project_menu_item(@project, params[:jump]) && return
-
end
-
-
62
@users_by_role = @project.users_by_role
-
62
@subprojects = @project.children.visible.all
-
62
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
-
62
@trackers = @project.rolled_up_trackers
-
-
62
cond = @project.project_condition(Setting.display_subprojects_issues?)
-
-
62
@open_issues_by_tracker = Issue.visible.open.where(cond).count(:group => :tracker)
-
62
@total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker)
-
-
62
if User.current.allowed_to?(:view_time_entries, @project)
-
62
@total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
-
end
-
-
62
@key = User.current.rss_key
-
-
62
respond_to do |format|
-
62
format.html
-
62
format.api
-
end
-
end
-
-
1
def settings
-
7
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
-
7
@issue_category ||= IssueCategory.new
-
7
@member ||= @project.members.new
-
7
@trackers = Tracker.sorted.all
-
7
@wiki ||= @project.wiki
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@project.safe_attributes = params[:project]
-
if validate_parent_id && @project.save
-
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'settings', :id => @project
-
}
-
format.api { render_api_ok }
-
end
-
else
-
respond_to do |format|
-
format.html {
-
settings
-
render :action => 'settings'
-
}
-
format.api { render_validation_errors(@project) }
-
end
-
end
-
end
-
-
1
def modules
-
@project.enabled_module_names = params[:enabled_module_names]
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'settings', :id => @project, :tab => 'modules'
-
end
-
-
1
def archive
-
if request.post?
-
unless @project.archive
-
flash[:error] = l(:error_can_not_archive_project)
-
end
-
end
-
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
-
end
-
-
1
def unarchive
-
@project.unarchive if request.post? && !@project.active?
-
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
-
end
-
-
1
def close
-
@project.close
-
redirect_to project_path(@project)
-
end
-
-
1
def reopen
-
@project.reopen
-
redirect_to project_path(@project)
-
end
-
-
# Delete @project
-
1
def destroy
-
@project_to_destroy = @project
-
if api_request? || params[:confirm]
-
@project_to_destroy.destroy
-
respond_to do |format|
-
format.html { redirect_to :controller => 'admin', :action => 'projects' }
-
format.api { render_api_ok }
-
end
-
end
-
# hide project in layout
-
@project = nil
-
end
-
-
1
private
-
-
# Validates parent_id param according to user's permissions
-
# TODO: move it to Project model in a validation that depends on User.current
-
1
def validate_parent_id
-
return true if User.current.admin?
-
parent_id = params[:project] && params[:project][:parent_id]
-
if parent_id || @project.new_record?
-
parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
-
unless @project.allowed_parents.include?(parent)
-
@project.errors.add :parent_id, :invalid
-
return false
-
end
-
end
-
true
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class QueriesController < ApplicationController
-
1
menu_item :issues
-
1
before_filter :find_query, :except => [:new, :create, :index]
-
1
before_filter :find_optional_project, :only => [:new, :create]
-
-
1
accept_api_auth :index
-
-
1
include QueriesHelper
-
-
1
def index
-
case params[:format]
-
when 'xml', 'json'
-
@offset, @limit = api_offset_and_limit
-
else
-
@limit = per_page_option
-
end
-
-
@query_count = Query.visible.count
-
@query_pages = Paginator.new self, @query_count, @limit, params['page']
-
@queries = Query.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name")
-
-
respond_to do |format|
-
format.html { render :nothing => true }
-
format.api
-
end
-
end
-
-
1
def new
-
@query = Query.new
-
@query.user = User.current
-
@query.project = @project
-
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
-
build_query_from_params
-
end
-
-
1
def create
-
@query = Query.new(params[:query])
-
@query.user = User.current
-
@query.project = params[:query_is_for_all] ? nil : @project
-
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
-
build_query_from_params
-
@query.column_names = nil if params[:default_columns]
-
-
if @query.save
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
-
else
-
render :action => 'new', :layout => !request.xhr?
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@query.attributes = params[:query]
-
@query.project = nil if params[:query_is_for_all]
-
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
-
build_query_from_params
-
@query.column_names = nil if params[:default_columns]
-
-
if @query.save
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@query.destroy
-
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
-
end
-
-
1
private
-
1
def find_query
-
@query = Query.find(params[:id])
-
@project = @query.project
-
render_403 unless @query.editable_by?(User.current)
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def find_optional_project
-
@project = Project.find(params[:project_id]) if params[:project_id]
-
render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ReportsController < ApplicationController
-
1
menu_item :issues
-
1
before_filter :find_project, :authorize, :find_issue_statuses
-
-
1
def issue_report
-
@trackers = @project.trackers
-
@versions = @project.shared_versions.sort
-
@priorities = IssuePriority.all.reverse
-
@categories = @project.issue_categories
-
@assignees = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
-
@authors = @project.users.sort
-
@subprojects = @project.descendants.visible
-
-
@issues_by_tracker = Issue.by_tracker(@project)
-
@issues_by_version = Issue.by_version(@project)
-
@issues_by_priority = Issue.by_priority(@project)
-
@issues_by_category = Issue.by_category(@project)
-
@issues_by_assigned_to = Issue.by_assigned_to(@project)
-
@issues_by_author = Issue.by_author(@project)
-
@issues_by_subproject = Issue.by_subproject(@project) || []
-
-
render :template => "reports/issue_report"
-
end
-
-
1
def issue_report_details
-
case params[:detail]
-
when "tracker"
-
@field = "tracker_id"
-
@rows = @project.trackers
-
@data = Issue.by_tracker(@project)
-
@report_title = l(:field_tracker)
-
when "version"
-
@field = "fixed_version_id"
-
@rows = @project.shared_versions.sort
-
@data = Issue.by_version(@project)
-
@report_title = l(:field_version)
-
when "priority"
-
@field = "priority_id"
-
@rows = IssuePriority.all.reverse
-
@data = Issue.by_priority(@project)
-
@report_title = l(:field_priority)
-
when "category"
-
@field = "category_id"
-
@rows = @project.issue_categories
-
@data = Issue.by_category(@project)
-
@report_title = l(:field_category)
-
when "assigned_to"
-
@field = "assigned_to_id"
-
@rows = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
-
@data = Issue.by_assigned_to(@project)
-
@report_title = l(:field_assigned_to)
-
when "author"
-
@field = "author_id"
-
@rows = @project.users.sort
-
@data = Issue.by_author(@project)
-
@report_title = l(:field_author)
-
when "subproject"
-
@field = "project_id"
-
@rows = @project.descendants.visible
-
@data = Issue.by_subproject(@project) || []
-
@report_title = l(:field_subproject)
-
end
-
-
respond_to do |format|
-
if @field
-
format.html {}
-
else
-
format.html { redirect_to :action => 'issue_report', :id => @project }
-
end
-
end
-
end
-
-
1
private
-
-
1
def find_issue_statuses
-
@statuses = IssueStatus.find(:all, :order => 'position')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'SVG/Graph/Bar'
-
1
require 'SVG/Graph/BarHorizontal'
-
1
require 'digest/sha1'
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
-
1
class ChangesetNotFound < Exception; end
-
1
class InvalidRevisionParam < Exception; end
-
-
1
class RepositoriesController < ApplicationController
-
1
menu_item :repository
-
1
menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
-
1
default_search_scope :changesets
-
-
1
before_filter :find_project_by_project_id, :only => [:new, :create]
-
1
before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
-
1
before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
-
1
before_filter :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
-
1
before_filter :authorize
-
1
accept_rss_auth :revisions
-
-
1
rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
-
-
1
def new
-
scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
-
@repository = Repository.factory(scm)
-
@repository.is_default = @project.repository.nil?
-
@repository.project = @project
-
end
-
-
1
def create
-
attrs = pickup_extra_info
-
@repository = Repository.factory(params[:repository_scm])
-
@repository.safe_attributes = params[:repository]
-
if attrs[:attrs_extra].keys.any?
-
@repository.merge_extra_info(attrs[:attrs_extra])
-
end
-
@repository.project = @project
-
if request.post? && @repository.save
-
redirect_to settings_project_path(@project, :tab => 'repositories')
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
attrs = pickup_extra_info
-
@repository.safe_attributes = attrs[:attrs]
-
if attrs[:attrs_extra].keys.any?
-
@repository.merge_extra_info(attrs[:attrs_extra])
-
end
-
@repository.project = @project
-
if request.put? && @repository.save
-
redirect_to settings_project_path(@project, :tab => 'repositories')
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def pickup_extra_info
-
p = {}
-
p_extra = {}
-
params[:repository].each do |k, v|
-
if k =~ /^extra_/
-
p_extra[k] = v
-
else
-
p[k] = v
-
end
-
end
-
{:attrs => p, :attrs_extra => p_extra}
-
end
-
1
private :pickup_extra_info
-
-
1
def committers
-
@committers = @repository.committers
-
@users = @project.users
-
additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
-
@users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
-
@users.compact!
-
@users.sort!
-
if request.post? && params[:committers].is_a?(Hash)
-
# Build a hash with repository usernames as keys and corresponding user ids as values
-
@repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to settings_project_path(@project, :tab => 'repositories')
-
end
-
end
-
-
1
def destroy
-
@repository.destroy if request.delete?
-
redirect_to settings_project_path(@project, :tab => 'repositories')
-
end
-
-
1
def show
-
@repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
-
-
@entries = @repository.entries(@path, @rev)
-
@changeset = @repository.find_changeset_by_name(@rev)
-
if request.xhr?
-
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
-
else
-
(show_error_not_found; return) unless @entries
-
@changesets = @repository.latest_changesets(@path, @rev)
-
@properties = @repository.properties(@path, @rev)
-
@repositories = @project.repositories
-
render :action => 'show'
-
end
-
end
-
-
1
alias_method :browse, :show
-
-
1
def changes
-
@entry = @repository.entry(@path, @rev)
-
(show_error_not_found; return) unless @entry
-
@changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
-
@properties = @repository.properties(@path, @rev)
-
@changeset = @repository.find_changeset_by_name(@rev)
-
end
-
-
1
def revisions
-
@changeset_count = @repository.changesets.count
-
@changeset_pages = Paginator.new self, @changeset_count,
-
per_page_option,
-
params['page']
-
@changesets = @repository.changesets.find(:all,
-
:limit => @changeset_pages.items_per_page,
-
:offset => @changeset_pages.current.offset,
-
:include => [:user, :repository, :parents])
-
-
respond_to do |format|
-
format.html { render :layout => false if request.xhr? }
-
format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
-
end
-
end
-
-
1
def raw
-
entry_and_raw(true)
-
end
-
-
1
def entry
-
entry_and_raw(false)
-
end
-
-
1
def entry_and_raw(is_raw)
-
@entry = @repository.entry(@path, @rev)
-
(show_error_not_found; return) unless @entry
-
-
# If the entry is a dir, show the browser
-
(show; return) if @entry.is_dir?
-
-
@content = @repository.cat(@path, @rev)
-
(show_error_not_found; return) unless @content
-
if is_raw ||
-
(@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
-
! is_entry_text_data?(@content, @path)
-
# Force the download
-
send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
-
send_type = Redmine::MimeType.of(@path)
-
send_opt[:type] = send_type.to_s if send_type
-
send_opt[:disposition] = (Redmine::MimeType.is_type?('image', @path) && !is_raw ? 'inline' : 'attachment')
-
send_data @content, send_opt
-
else
-
# Prevent empty lines when displaying a file with Windows style eol
-
# TODO: UTF-16
-
# Is this needs? AttachmentsController reads file simply.
-
@content.gsub!("\r\n", "\n")
-
@changeset = @repository.find_changeset_by_name(@rev)
-
end
-
end
-
1
private :entry_and_raw
-
-
1
def is_entry_text_data?(ent, path)
-
# UTF-16 contains "\x00".
-
# It is very strict that file contains less than 30% of ascii symbols
-
# in non Western Europe.
-
return true if Redmine::MimeType.is_type?('text', path)
-
# Ruby 1.8.6 has a bug of integer divisions.
-
# http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
-
return false if ent.is_binary_data?
-
true
-
end
-
1
private :is_entry_text_data?
-
-
1
def annotate
-
@entry = @repository.entry(@path, @rev)
-
(show_error_not_found; return) unless @entry
-
-
@annotate = @repository.scm.annotate(@path, @rev)
-
if @annotate.nil? || @annotate.empty?
-
(render_error l(:error_scm_annotate); return)
-
end
-
ann_buf_size = 0
-
@annotate.lines.each do |buf|
-
ann_buf_size += buf.size
-
end
-
if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
-
(render_error l(:error_scm_annotate_big_text_file); return)
-
end
-
@changeset = @repository.find_changeset_by_name(@rev)
-
end
-
-
1
def revision
-
respond_to do |format|
-
format.html
-
format.js {render :layout => false}
-
end
-
end
-
-
# Adds a related issue to a changeset
-
# POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
-
1
def add_related_issue
-
@issue = @changeset.find_referenced_issue_by_id(params[:issue_id])
-
if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
-
@issue = nil
-
end
-
-
if @issue
-
@changeset.issues << @issue
-
end
-
end
-
-
# Removes a related issue from a changeset
-
# DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
-
1
def remove_related_issue
-
@issue = Issue.visible.find_by_id(params[:issue_id])
-
if @issue
-
@changeset.issues.delete(@issue)
-
end
-
end
-
-
1
def diff
-
if params[:format] == 'diff'
-
@diff = @repository.diff(@path, @rev, @rev_to)
-
(show_error_not_found; return) unless @diff
-
filename = "changeset_r#{@rev}"
-
filename << "_r#{@rev_to}" if @rev_to
-
send_data @diff.join, :filename => "#{filename}.diff",
-
:type => 'text/x-patch',
-
:disposition => 'attachment'
-
else
-
@diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
-
@diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
-
-
# Save diff type as user preference
-
if User.current.logged? && @diff_type != User.current.pref[:diff_type]
-
User.current.pref[:diff_type] = @diff_type
-
User.current.preference.save
-
end
-
@cache_key = "repositories/diff/#{@repository.id}/" +
-
Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
-
unless read_fragment(@cache_key)
-
@diff = @repository.diff(@path, @rev, @rev_to)
-
show_error_not_found unless @diff
-
end
-
-
@changeset = @repository.find_changeset_by_name(@rev)
-
@changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
-
@diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
-
end
-
end
-
-
1
def stats
-
end
-
-
1
def graph
-
data = nil
-
case params[:graph]
-
when "commits_per_month"
-
data = graph_commits_per_month(@repository)
-
when "commits_per_author"
-
data = graph_commits_per_author(@repository)
-
end
-
if data
-
headers["Content-Type"] = "image/svg+xml"
-
send_data(data, :type => "image/svg+xml", :disposition => "inline")
-
else
-
render_404
-
end
-
end
-
-
1
private
-
-
1
def find_repository
-
@repository = Repository.find(params[:id])
-
@project = @repository.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
-
-
1
def find_project_repository
-
@project = Project.find(params[:id])
-
if params[:repository_id].present?
-
@repository = @project.repositories.find_by_identifier_param(params[:repository_id])
-
else
-
@repository = @project.repository
-
end
-
(render_404; return false) unless @repository
-
@path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
-
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
-
@rev_to = params[:rev_to]
-
-
unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
-
if @repository.branches.blank?
-
raise InvalidRevisionParam
-
end
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
rescue InvalidRevisionParam
-
show_error_not_found
-
end
-
-
1
def find_changeset
-
if @rev.present?
-
@changeset = @repository.find_changeset_by_name(@rev)
-
end
-
show_error_not_found unless @changeset
-
end
-
-
1
def show_error_not_found
-
render_error :message => l(:error_scm_not_found), :status => 404
-
end
-
-
# Handler for Redmine::Scm::Adapters::CommandFailed exception
-
1
def show_error_command_failed(exception)
-
render_error l(:error_scm_command_failed, exception.message)
-
end
-
-
1
def graph_commits_per_month(repository)
-
@date_to = Date.today
-
@date_from = @date_to << 11
-
@date_from = Date.civil(@date_from.year, @date_from.month, 1)
-
commits_by_day = Changeset.count(
-
:all, :group => :commit_date,
-
:conditions => ["repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
-
commits_by_month = [0] * 12
-
commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
-
-
changes_by_day = Change.count(
-
:all, :group => :commit_date, :include => :changeset,
-
:conditions => ["#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
-
changes_by_month = [0] * 12
-
changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
-
-
fields = []
-
12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
-
-
graph = SVG::Graph::Bar.new(
-
:height => 300,
-
:width => 800,
-
:fields => fields.reverse,
-
:stack => :side,
-
:scale_integers => true,
-
:step_x_labels => 2,
-
:show_data_values => false,
-
:graph_title => l(:label_commits_per_month),
-
:show_graph_title => true
-
)
-
-
graph.add_data(
-
:data => commits_by_month[0..11].reverse,
-
:title => l(:label_revision_plural)
-
)
-
-
graph.add_data(
-
:data => changes_by_month[0..11].reverse,
-
:title => l(:label_change_plural)
-
)
-
-
graph.burn
-
end
-
-
1
def graph_commits_per_author(repository)
-
commits_by_author = Changeset.count(:all, :group => :committer, :conditions => ["repository_id = ?", repository.id])
-
commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
-
-
changes_by_author = Change.count(:all, :group => :committer, :include => :changeset, :conditions => ["#{Changeset.table_name}.repository_id = ?", repository.id])
-
h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
-
-
fields = commits_by_author.collect {|r| r.first}
-
commits_data = commits_by_author.collect {|r| r.last}
-
changes_data = commits_by_author.collect {|r| h[r.first] || 0}
-
-
fields = fields + [""]*(10 - fields.length) if fields.length<10
-
commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
-
changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
-
-
# Remove email adress in usernames
-
fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
-
-
graph = SVG::Graph::BarHorizontal.new(
-
:height => 400,
-
:width => 800,
-
:fields => fields,
-
:stack => :side,
-
:scale_integers => true,
-
:show_data_values => false,
-
:rotate_y_labels => false,
-
:graph_title => l(:label_commits_per_author),
-
:show_graph_title => true
-
)
-
graph.add_data(
-
:data => commits_data,
-
:title => l(:label_revision_plural)
-
)
-
graph.add_data(
-
:data => changes_data,
-
:title => l(:label_change_plural)
-
)
-
graph.burn
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class RolesController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin, :except => [:index, :show]
-
1
before_filter :require_admin_or_api_request, :only => [:index, :show]
-
1
before_filter :find_role, :only => [:show, :edit, :update, :destroy]
-
1
accept_api_auth :index, :show
-
-
1
def index
-
respond_to do |format|
-
format.html {
-
@role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position'
-
render :action => "index", :layout => false if request.xhr?
-
}
-
format.api {
-
@roles = Role.givable.all
-
}
-
end
-
end
-
-
1
def show
-
respond_to do |format|
-
format.api
-
end
-
end
-
-
1
def new
-
# Prefills the form with 'Non member' role permissions by default
-
@role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
-
if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy])
-
@role.copy_from(@copy_from)
-
end
-
@roles = Role.sorted.all
-
end
-
-
1
def create
-
@role = Role.new(params[:role])
-
if request.post? && @role.save
-
# workflow copy
-
if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
-
@role.workflow_rules.copy(copy_from)
-
end
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index'
-
else
-
@roles = Role.sorted.all
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
if request.put? and @role.update_attributes(params[:role])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index'
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@role.destroy
-
redirect_to :action => 'index'
-
rescue
-
flash[:error] = l(:error_can_not_remove_role)
-
redirect_to :action => 'index'
-
end
-
-
1
def permissions
-
@roles = Role.sorted.all
-
@permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
-
if request.post?
-
@roles.each do |role|
-
role.permissions = params[:permissions][role.id.to_s]
-
role.save
-
end
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index'
-
end
-
end
-
-
1
private
-
-
1
def find_role
-
@role = Role.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class SearchController < ApplicationController
-
1
before_filter :find_optional_project
-
-
1
helper :messages
-
1
include MessagesHelper
-
-
1
def index
-
@question = params[:q] || ""
-
@question.strip!
-
@all_words = params[:all_words] ? params[:all_words].present? : true
-
@titles_only = params[:titles_only] ? params[:titles_only].present? : false
-
-
projects_to_search =
-
case params[:scope]
-
when 'all'
-
nil
-
when 'my_projects'
-
User.current.memberships.collect(&:project)
-
when 'subprojects'
-
@project ? (@project.self_and_descendants.active.all) : nil
-
else
-
@project
-
end
-
-
offset = nil
-
begin; offset = params[:offset].to_time if params[:offset]; rescue; end
-
-
# quick jump to an issue
-
if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1.to_i)
-
redirect_to :controller => "issues", :action => "show", :id => $1
-
return
-
end
-
-
@object_types = Redmine::Search.available_search_types.dup
-
if projects_to_search.is_a? Project
-
# don't search projects
-
@object_types.delete('projects')
-
# only show what the user is allowed to view
-
@object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
-
end
-
-
@scope = @object_types.select {|t| params[t]}
-
@scope = @object_types if @scope.empty?
-
-
# extract tokens from the question
-
# eg. hello "bye bye" => ["hello", "bye bye"]
-
@tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
-
# tokens must be at least 2 characters long
-
@tokens = @tokens.uniq.select {|w| w.length > 1 }
-
-
if !@tokens.empty?
-
# no more than 5 tokens to search for
-
@tokens.slice! 5..-1 if @tokens.size > 5
-
-
@results = []
-
@results_by_type = Hash.new {|h,k| h[k] = 0}
-
-
limit = 10
-
@scope.each do |s|
-
r, c = s.singularize.camelcase.constantize.search(@tokens, projects_to_search,
-
:all_words => @all_words,
-
:titles_only => @titles_only,
-
:limit => (limit+1),
-
:offset => offset,
-
:before => params[:previous].nil?)
-
@results += r
-
@results_by_type[s] += c
-
end
-
@results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
-
if params[:previous].nil?
-
@pagination_previous_date = @results[0].event_datetime if offset && @results[0]
-
if @results.size > limit
-
@pagination_next_date = @results[limit-1].event_datetime
-
@results = @results[0, limit]
-
end
-
else
-
@pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
-
if @results.size > limit
-
@pagination_previous_date = @results[-(limit)].event_datetime
-
@results = @results[-(limit), limit]
-
end
-
end
-
else
-
@question = ""
-
end
-
render :layout => false if request.xhr?
-
end
-
-
1
private
-
1
def find_optional_project
-
return true unless params[:id]
-
@project = Project.find(params[:id])
-
check_project_privacy
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class SettingsController < ApplicationController
-
1
layout 'admin'
-
1
menu_item :plugins, :only => :plugin
-
-
1
before_filter :require_admin
-
-
1
def index
-
edit
-
render :action => 'edit'
-
end
-
-
1
def edit
-
@notifiables = Redmine::Notifiable.all
-
if request.post? && params[:settings] && params[:settings].is_a?(Hash)
-
settings = (params[:settings] || {}).dup.symbolize_keys
-
settings.each do |name, value|
-
# remove blank values in array settings
-
value.delete_if {|v| v.blank? } if value.is_a?(Array)
-
Setting[name] = value
-
end
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'edit', :tab => params[:tab]
-
else
-
@options = {}
-
user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]}
-
@options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]}
-
@deliveries = ActionMailer::Base.perform_deliveries
-
-
@guessed_host_and_path = request.host_with_port.dup
-
@guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
-
-
Redmine::Themes.rescan
-
end
-
end
-
-
1
def plugin
-
1
@plugin = Redmine::Plugin.find(params[:id])
-
1
if request.post?
-
Setting.send "plugin_#{@plugin.id}=", params[:settings]
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'plugin', :id => @plugin.id
-
else
-
1
@partial = @plugin.settings[:partial]
-
1
@settings = Setting.send "plugin_#{@plugin.id}"
-
end
-
rescue Redmine::PluginNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class SysController < ActionController::Base
-
1
before_filter :check_enabled
-
-
1
def projects
-
p = Project.active.has_module(:repository).find(
-
:all,
-
:include => :repository,
-
:order => "#{Project.table_name}.identifier"
-
)
-
# extra_info attribute from repository breaks activeresource client
-
render :xml => p.to_xml(
-
:only => [:id, :identifier, :name, :is_public, :status],
-
:include => {:repository => {:only => [:id, :url]}}
-
)
-
end
-
-
1
def create_project_repository
-
project = Project.find(params[:id])
-
if project.repository
-
render :nothing => true, :status => 409
-
else
-
logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
-
repository = Repository.factory(params[:vendor], params[:repository])
-
repository.project = project
-
if repository.save
-
render :xml => {repository.class.name.underscore.gsub('/', '-') => {:id => repository.id, :url => repository.url}}, :status => 201
-
else
-
render :nothing => true, :status => 422
-
end
-
end
-
end
-
-
1
def fetch_changesets
-
projects = []
-
scope = Project.active.has_module(:repository)
-
if params[:id]
-
project = nil
-
if params[:id].to_s =~ /^\d*$/
-
project = scope.find(params[:id])
-
else
-
project = scope.find_by_identifier(params[:id])
-
end
-
raise ActiveRecord::RecordNotFound unless project
-
projects << project
-
else
-
projects = scope.all
-
end
-
projects.each do |project|
-
project.repositories.each do |repository|
-
repository.fetch_changesets
-
end
-
end
-
render :nothing => true, :status => 200
-
rescue ActiveRecord::RecordNotFound
-
render :nothing => true, :status => 404
-
end
-
-
1
protected
-
-
1
def check_enabled
-
User.current = nil
-
unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
-
render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
-
return false
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TimelogController < ApplicationController
-
1
menu_item :issues
-
-
1
before_filter :find_project_for_new_time_entry, :only => [:create]
-
1
before_filter :find_time_entry, :only => [:show, :edit, :update]
-
1
before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
-
1
before_filter :authorize, :except => [:new, :index, :report]
-
-
1
before_filter :find_optional_project, :only => [:index, :report]
-
1
before_filter :find_optional_project_for_new_time_entry, :only => [:new]
-
1
before_filter :authorize_global, :only => [:new, :index, :report]
-
-
1
accept_rss_auth :index
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
helper :sort
-
1
include SortHelper
-
1
helper :issues
-
1
include TimelogHelper
-
1
helper :custom_fields
-
1
include CustomFieldsHelper
-
-
1
def index
-
4
sort_init 'spent_on', 'desc'
-
sort_update 'spent_on' => ['spent_on', "#{TimeEntry.table_name}.created_on"],
-
'user' => 'user_id',
-
'activity' => 'activity_id',
-
'project' => "#{Project.table_name}.name",
-
'issue' => 'issue_id',
-
4
'hours' => 'hours'
-
-
4
retrieve_date_range
-
-
4
scope = TimeEntry.visible.spent_between(@from, @to)
-
4
if @issue
-
2
scope = scope.on_issue(@issue)
-
elsif @project
-
2
scope = scope.on_project(@project, Setting.display_subprojects_issues?)
-
end
-
-
4
respond_to do |format|
-
4
format.html {
-
# Paginate results
-
4
@entry_count = scope.count
-
4
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
-
4
@entries = scope.all(
-
:include => [:project, :activity, :user, {:issue => :tracker}],
-
:order => sort_clause,
-
:limit => @entry_pages.items_per_page,
-
:offset => @entry_pages.current.offset
-
)
-
4
@total_hours = scope.sum(:hours).to_f
-
-
4
render :layout => !request.xhr?
-
}
-
4
format.api {
-
@entry_count = scope.count
-
@offset, @limit = api_offset_and_limit
-
@entries = scope.all(
-
:include => [:project, :activity, :user, {:issue => :tracker}],
-
:order => sort_clause,
-
:limit => @limit,
-
:offset => @offset
-
)
-
}
-
4
format.atom {
-
entries = scope.all(
-
:include => [:project, :activity, :user, {:issue => :tracker}],
-
:order => "#{TimeEntry.table_name}.created_on DESC",
-
:limit => Setting.feeds_limit.to_i
-
)
-
render_feed(entries, :title => l(:label_spent_time))
-
}
-
4
format.csv {
-
# Export all entries
-
@entries = scope.all(
-
:include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
-
:order => sort_clause
-
)
-
send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
-
}
-
end
-
end
-
-
1
def report
-
retrieve_date_range
-
@report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to)
-
-
respond_to do |format|
-
format.html { render :layout => !request.xhr? }
-
format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
-
end
-
end
-
-
1
def show
-
respond_to do |format|
-
# TODO: Implement html response
-
format.html { render :nothing => true, :status => 406 }
-
format.api
-
end
-
end
-
-
1
def new
-
2
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
-
2
@time_entry.safe_attributes = params[:time_entry]
-
end
-
-
1
def create
-
2
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
-
2
@time_entry.safe_attributes = params[:time_entry]
-
-
2
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
-
-
2
if @time_entry.save
-
2
respond_to do |format|
-
2
format.html {
-
2
flash[:notice] = l(:notice_successful_create)
-
2
if params[:continue]
-
if params[:project_id]
-
redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue,
-
:time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
-
:back_url => params[:back_url]
-
else
-
redirect_to :action => 'new',
-
:time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
-
:back_url => params[:back_url]
-
end
-
else
-
2
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
-
end
-
}
-
2
format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'new' }
-
format.api { render_validation_errors(@time_entry) }
-
end
-
end
-
end
-
-
1
def edit
-
@time_entry.safe_attributes = params[:time_entry]
-
end
-
-
1
def update
-
@time_entry.safe_attributes = params[:time_entry]
-
-
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
-
-
if @time_entry.save
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_update)
-
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
-
}
-
format.api { render_api_ok }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'edit' }
-
format.api { render_validation_errors(@time_entry) }
-
end
-
end
-
end
-
-
1
def bulk_edit
-
@available_activities = TimeEntryActivity.shared.active
-
@custom_fields = TimeEntry.first.available_custom_fields
-
end
-
-
1
def bulk_update
-
attributes = parse_params_for_bulk_time_entry_attributes(params)
-
-
unsaved_time_entry_ids = []
-
@time_entries.each do |time_entry|
-
time_entry.reload
-
time_entry.safe_attributes = attributes
-
call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
-
unless time_entry.save
-
# Keep unsaved time_entry ids to display them in flash error
-
unsaved_time_entry_ids << time_entry.id
-
end
-
end
-
set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
-
redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
-
end
-
-
1
def destroy
-
destroyed = TimeEntry.transaction do
-
@time_entries.each do |t|
-
unless t.destroy && t.destroyed?
-
raise ActiveRecord::Rollback
-
end
-
end
-
end
-
-
respond_to do |format|
-
format.html {
-
if destroyed
-
flash[:notice] = l(:notice_successful_delete)
-
else
-
flash[:error] = l(:notice_unable_delete_time_entry)
-
end
-
redirect_back_or_default(:action => 'index', :project_id => @projects.first)
-
}
-
format.api {
-
if destroyed
-
render_api_ok
-
else
-
render_validation_errors(@time_entries)
-
end
-
}
-
end
-
end
-
-
1
private
-
1
def find_time_entry
-
@time_entry = TimeEntry.find(params[:id])
-
unless @time_entry.editable_by?(User.current)
-
render_403
-
return false
-
end
-
@project = @time_entry.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def find_time_entries
-
@time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
-
raise ActiveRecord::RecordNotFound if @time_entries.empty?
-
@projects = @time_entries.collect(&:project).compact.uniq
-
@project = @projects.first if @projects.size == 1
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
-
if unsaved_time_entry_ids.empty?
-
flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
-
else
-
flash[:error] = l(:notice_failed_to_save_time_entries,
-
:count => unsaved_time_entry_ids.size,
-
:total => time_entries.size,
-
:ids => '#' + unsaved_time_entry_ids.join(', #'))
-
end
-
end
-
-
1
def find_optional_project_for_new_time_entry
-
4
if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
-
4
@project = Project.find(project_id)
-
end
-
4
if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
-
2
@issue = Issue.find(issue_id)
-
2
@project ||= @issue.project
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def find_project_for_new_time_entry
-
2
find_optional_project_for_new_time_entry
-
2
if @project.nil?
-
render_404
-
end
-
end
-
-
1
def find_optional_project
-
4
if !params[:issue_id].blank?
-
2
@issue = Issue.find(params[:issue_id])
-
2
@project = @issue.project
-
2
elsif !params[:project_id].blank?
-
2
@project = Project.find(params[:project_id])
-
end
-
end
-
-
# Retrieves the date range based on predefined ranges or specific from/to param dates
-
1
def retrieve_date_range
-
4
@free_period = false
-
4
@from, @to = nil, nil
-
-
4
if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
-
case params[:period].to_s
-
when 'today'
-
@from = @to = Date.today
-
when 'yesterday'
-
@from = @to = Date.today - 1
-
when 'current_week'
-
@from = Date.today - (Date.today.cwday - 1)%7
-
@to = @from + 6
-
when 'last_week'
-
@from = Date.today - 7 - (Date.today.cwday - 1)%7
-
@to = @from + 6
-
when 'last_2_weeks'
-
@from = Date.today - 14 - (Date.today.cwday - 1)%7
-
@to = @from + 13
-
when '7_days'
-
@from = Date.today - 7
-
@to = Date.today
-
when 'current_month'
-
@from = Date.civil(Date.today.year, Date.today.month, 1)
-
@to = (@from >> 1) - 1
-
when 'last_month'
-
@from = Date.civil(Date.today.year, Date.today.month, 1) << 1
-
@to = (@from >> 1) - 1
-
when '30_days'
-
@from = Date.today - 30
-
@to = Date.today
-
when 'current_year'
-
@from = Date.civil(Date.today.year, 1, 1)
-
@to = Date.civil(Date.today.year, 12, 31)
-
end
-
elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
-
begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
-
begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
-
@free_period = true
-
else
-
# default
-
end
-
-
4
@from, @to = @to, @from if @from && @to && @from > @to
-
end
-
-
1
def parse_params_for_bulk_time_entry_attributes(params)
-
attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
-
attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
-
attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
-
attributes
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TrackersController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin, :except => :index
-
1
before_filter :require_admin_or_api_request, :only => :index
-
1
accept_api_auth :index
-
-
1
def index
-
respond_to do |format|
-
format.html {
-
@tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
-
render :action => "index", :layout => false if request.xhr?
-
}
-
format.api {
-
@trackers = Tracker.sorted.all
-
}
-
end
-
end
-
-
1
def new
-
@tracker ||= Tracker.new(params[:tracker])
-
@trackers = Tracker.find :all, :order => 'position'
-
@projects = Project.find(:all)
-
end
-
-
1
def create
-
@tracker = Tracker.new(params[:tracker])
-
if request.post? and @tracker.save
-
# workflow copy
-
if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
-
@tracker.workflow_rules.copy(copy_from)
-
end
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index'
-
return
-
end
-
new
-
render :action => 'new'
-
end
-
-
1
def edit
-
@tracker ||= Tracker.find(params[:id])
-
@projects = Project.find(:all)
-
end
-
-
1
def update
-
@tracker = Tracker.find(params[:id])
-
if request.put? and @tracker.update_attributes(params[:tracker])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index'
-
return
-
end
-
edit
-
render :action => 'edit'
-
end
-
-
1
def destroy
-
@tracker = Tracker.find(params[:id])
-
unless @tracker.issues.empty?
-
flash[:error] = l(:error_can_not_delete_tracker)
-
else
-
@tracker.destroy
-
end
-
redirect_to :action => 'index'
-
end
-
-
1
def fields
-
if request.post? && params[:trackers]
-
params[:trackers].each do |tracker_id, tracker_params|
-
tracker = Tracker.find_by_id(tracker_id)
-
if tracker
-
tracker.core_fields = tracker_params[:core_fields]
-
tracker.custom_field_ids = tracker_params[:custom_field_ids]
-
tracker.save
-
end
-
end
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'fields'
-
return
-
end
-
@trackers = Tracker.sorted.all
-
@custom_fields = IssueCustomField.all.sort
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class UsersController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin, :except => :show
-
1
before_filter :find_user, :only => [:show, :edit, :update, :destroy, :edit_membership, :destroy_membership]
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
helper :sort
-
1
include SortHelper
-
1
helper :custom_fields
-
1
include CustomFieldsHelper
-
-
1
def index
-
sort_init 'login', 'asc'
-
sort_update %w(login firstname lastname mail admin created_on last_login_on)
-
-
case params[:format]
-
when 'xml', 'json'
-
@offset, @limit = api_offset_and_limit
-
else
-
@limit = per_page_option
-
end
-
-
@status = params[:status] || 1
-
-
scope = User.logged.status(@status)
-
scope = scope.like(params[:name]) if params[:name].present?
-
scope = scope.in_group(params[:group_id]) if params[:group_id].present?
-
-
@user_count = scope.count
-
@user_pages = Paginator.new self, @user_count, @limit, params['page']
-
@offset ||= @user_pages.current.offset
-
@users = scope.find :all,
-
:order => sort_clause,
-
:limit => @limit,
-
:offset => @offset
-
-
respond_to do |format|
-
format.html {
-
@groups = Group.all.sort
-
render :layout => !request.xhr?
-
}
-
format.api
-
end
-
end
-
-
1
def show
-
# show projects based on current user visibility
-
@memberships = @user.memberships.all(:conditions => Project.visible_condition(User.current))
-
-
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
-
@events_by_day = events.group_by(&:event_date)
-
-
unless User.current.admin?
-
if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
-
render_404
-
return
-
end
-
end
-
-
respond_to do |format|
-
format.html { render :layout => 'base' }
-
format.api
-
end
-
end
-
-
1
def new
-
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
-
@auth_sources = AuthSource.find(:all)
-
end
-
-
1
def create
-
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
-
@user.safe_attributes = params[:user]
-
@user.admin = params[:user][:admin] || false
-
@user.login = params[:user][:login]
-
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
-
-
if @user.save
-
@user.pref.attributes = params[:pref]
-
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
-
@user.pref.save
-
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
-
-
Mailer.account_information(@user, params[:user][:password]).deliver if params[:send_information]
-
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
-
redirect_to(params[:continue] ?
-
{:controller => 'users', :action => 'new'} :
-
{:controller => 'users', :action => 'edit', :id => @user}
-
)
-
}
-
format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
-
end
-
else
-
@auth_sources = AuthSource.find(:all)
-
# Clear password input
-
@user.password = @user.password_confirmation = nil
-
-
respond_to do |format|
-
format.html { render :action => 'new' }
-
format.api { render_validation_errors(@user) }
-
end
-
end
-
end
-
-
1
def edit
-
@auth_sources = AuthSource.find(:all)
-
@membership ||= Member.new
-
end
-
-
1
def update
-
@user.admin = params[:user][:admin] if params[:user][:admin]
-
@user.login = params[:user][:login] if params[:user][:login]
-
if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
-
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
-
end
-
@user.safe_attributes = params[:user]
-
# Was the account actived ? (do it before User#save clears the change)
-
was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
-
# TODO: Similar to My#account
-
@user.pref.attributes = params[:pref]
-
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
-
-
if @user.save
-
@user.pref.save
-
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
-
-
if was_activated
-
Mailer.account_activated(@user).deliver
-
elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
-
Mailer.account_information(@user, params[:user][:password]).deliver
-
end
-
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to_referer_or edit_user_path(@user)
-
}
-
format.api { render_api_ok }
-
end
-
else
-
@auth_sources = AuthSource.find(:all)
-
@membership ||= Member.new
-
# Clear password input
-
@user.password = @user.password_confirmation = nil
-
-
respond_to do |format|
-
format.html { render :action => :edit }
-
format.api { render_validation_errors(@user) }
-
end
-
end
-
end
-
-
1
def destroy
-
@user.destroy
-
respond_to do |format|
-
format.html { redirect_back_or_default(users_url) }
-
format.api { render_api_ok }
-
end
-
end
-
-
1
def edit_membership
-
@membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
-
@membership.save
-
respond_to do |format|
-
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
-
format.js
-
end
-
end
-
-
1
def destroy_membership
-
@membership = Member.find(params[:membership_id])
-
if @membership.deletable?
-
@membership.destroy
-
end
-
respond_to do |format|
-
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
-
format.js
-
end
-
end
-
-
1
private
-
-
1
def find_user
-
if params[:id] == 'current'
-
require_login || return
-
@user = User.current
-
else
-
@user = User.find(params[:id])
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class VersionsController < ApplicationController
-
1
menu_item :roadmap
-
1
model_object Version
-
1
before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
-
1
before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
-
1
before_filter :find_project_by_project_id, :only => [:index, :new, :create, :close_completed]
-
1
before_filter :authorize
-
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
helper :custom_fields
-
1
helper :projects
-
-
1
def index
-
respond_to do |format|
-
format.html {
-
@trackers = @project.trackers.find(:all, :order => 'position')
-
retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
-
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
-
project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
-
-
@versions = @project.shared_versions || []
-
@versions += @project.rolled_up_versions.visible if @with_subprojects
-
@versions = @versions.uniq.sort
-
unless params[:completed]
-
@completed_versions = @versions.select {|version| version.closed? || version.completed? }
-
@versions -= @completed_versions
-
end
-
-
@issues_by_version = {}
-
if @selected_tracker_ids.any? && @versions.any?
-
issues = Issue.visible.all(
-
:include => [:project, :status, :tracker, :priority, :fixed_version],
-
:conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)},
-
:order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id"
-
)
-
@issues_by_version = issues.group_by(&:fixed_version)
-
end
-
@versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
-
}
-
format.api {
-
@versions = @project.shared_versions.all
-
}
-
end
-
end
-
-
1
def show
-
1
respond_to do |format|
-
1
format.html {
-
1
@issues = @version.fixed_issues.visible.find(:all,
-
:include => [:status, :tracker, :priority],
-
:order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
-
}
-
1
format.api
-
end
-
end
-
-
1
def new
-
@version = @project.versions.build
-
@version.safe_attributes = params[:version]
-
-
respond_to do |format|
-
format.html
-
format.js
-
end
-
end
-
-
1
def create
-
@version = @project.versions.build
-
if params[:version]
-
attributes = params[:version].dup
-
attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
-
@version.safe_attributes = attributes
-
end
-
-
if request.post?
-
if @version.save
-
respond_to do |format|
-
format.html do
-
flash[:notice] = l(:notice_successful_create)
-
redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
-
end
-
format.js
-
format.api do
-
render :action => 'show', :status => :created, :location => version_url(@version)
-
end
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'new' }
-
format.js { render :action => 'new' }
-
format.api { render_validation_errors(@version) }
-
end
-
end
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
if request.put? && params[:version]
-
attributes = params[:version].dup
-
attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
-
@version.safe_attributes = attributes
-
if @version.save
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_update)
-
redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
-
}
-
format.api { render_api_ok }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'edit' }
-
format.api { render_validation_errors(@version) }
-
end
-
end
-
end
-
end
-
-
1
def close_completed
-
if request.put?
-
@project.close_completed_versions
-
end
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
-
end
-
-
1
def destroy
-
if @version.fixed_issues.empty?
-
@version.destroy
-
respond_to do |format|
-
format.html { redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project }
-
format.api { render_api_ok }
-
end
-
else
-
respond_to do |format|
-
format.html {
-
flash[:error] = l(:notice_unable_delete_version)
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
-
}
-
format.api { head :unprocessable_entity }
-
end
-
end
-
end
-
-
1
def status_by
-
respond_to do |format|
-
format.html { render :action => 'show' }
-
format.js
-
end
-
end
-
-
1
private
-
-
1
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
-
if ids = params[:tracker_ids]
-
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
-
else
-
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WatchersController < ApplicationController
-
1
before_filter :find_project
-
1
before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
-
1
before_filter :authorize, :only => [:new, :destroy]
-
-
1
def watch
-
if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
-
render_403
-
else
-
set_watcher(User.current, true)
-
end
-
end
-
-
1
def unwatch
-
set_watcher(User.current, false)
-
end
-
-
1
def new
-
end
-
-
1
def create
-
if params[:watcher].is_a?(Hash) && request.post?
-
user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
-
user_ids.each do |user_id|
-
Watcher.create(:watchable => @watched, :user_id => user_id)
-
end
-
end
-
respond_to do |format|
-
format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
-
format.js
-
end
-
end
-
-
1
def append
-
if params[:watcher].is_a?(Hash)
-
user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
-
@users = User.active.find_all_by_id(user_ids)
-
end
-
end
-
-
1
def destroy
-
@watched.set_watcher(User.find(params[:user_id]), false) if request.post?
-
respond_to do |format|
-
format.html { redirect_to :back }
-
format.js
-
end
-
end
-
-
1
def autocomplete_for_user
-
@users = User.active.like(params[:q]).find(:all, :limit => 100)
-
if @watched
-
@users -= @watched.watcher_users
-
end
-
render :layout => false
-
end
-
-
1
private
-
1
def find_project
-
if params[:object_type] && params[:object_id]
-
klass = Object.const_get(params[:object_type].camelcase)
-
return false unless klass.respond_to?('watched_by')
-
@watched = klass.find(params[:object_id])
-
@project = @watched.project
-
elsif params[:project_id]
-
@project = Project.visible.find_by_param(params[:project_id])
-
end
-
rescue
-
render_404
-
end
-
-
1
def set_watcher(user, watching)
-
@watched.set_watcher(user, watching)
-
respond_to do |format|
-
format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
-
format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => @watched} }
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WelcomeController < ApplicationController
-
1
caches_action :robots
-
-
1
def index
-
5
@news = News.latest User.current
-
5
@projects = Project.latest User.current
-
end
-
-
1
def robots
-
@projects = Project.all_public.active
-
render :layout => false, :content_type => 'text/plain'
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'diff'
-
-
# The WikiController follows the Rails REST controller pattern but with
-
# a few differences
-
#
-
# * index - shows a list of WikiPages grouped by page or date
-
# * new - not used
-
# * create - not used
-
# * show - will also show the form for creating a new wiki page
-
# * edit - used to edit an existing or new page
-
# * update - used to save a wiki page update to the database, including new pages
-
# * destroy - normal
-
#
-
# Other member and collection methods are also used
-
#
-
# TODO: still being worked on
-
1
class WikiController < ApplicationController
-
1
default_search_scope :wiki_pages
-
1
before_filter :find_wiki, :authorize
-
1
before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
-
1
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
-
1
accept_api_auth :index, :show, :update, :destroy
-
-
1
helper :attachments
-
1
include AttachmentsHelper
-
1
helper :watchers
-
1
include Redmine::Export::PDF
-
-
# List of pages, sorted alphabetically and by parent (hierarchy)
-
1
def index
-
load_pages_for_index
-
-
respond_to do |format|
-
format.html {
-
@pages_by_parent_id = @pages.group_by(&:parent_id)
-
}
-
format.api
-
end
-
end
-
-
# List of page, by last update
-
1
def date_index
-
load_pages_for_index
-
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
-
end
-
-
# display a page (in editing mode if it doesn't exist)
-
1
def show
-
2
if @page.new_record?
-
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
-
edit
-
render :action => 'edit'
-
else
-
render_404
-
end
-
return
-
end
-
2
if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
-
deny_access
-
return
-
end
-
2
@content = @page.content_for_version(params[:version])
-
2
if User.current.allowed_to?(:export_wiki_pages, @project)
-
2
if params[:format] == 'pdf'
-
send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
-
return
-
elsif params[:format] == 'html'
-
export = render_to_string :action => 'export', :layout => false
-
send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
-
return
-
elsif params[:format] == 'txt'
-
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
-
return
-
end
-
end
-
2
@editable = editable?
-
2
@sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
-
@content.current_version? &&
-
Redmine::WikiFormatting.supports_section_edit?
-
-
2
respond_to do |format|
-
2
format.html
-
2
format.api
-
end
-
end
-
-
# edit an existing page or a new one
-
1
def edit
-
2
return render_403 unless editable?
-
2
if @page.new_record?
-
@page.content = WikiContent.new(:page => @page)
-
if params[:parent].present?
-
@page.parent = @page.wiki.find_page(params[:parent].to_s)
-
end
-
end
-
-
2
@content = @page.content_for_version(params[:version])
-
2
@content.text = initial_page_content(@page) if @content.text.blank?
-
# don't keep previous comment
-
2
@content.comments = nil
-
-
# To prevent StaleObjectError exception when reverting to a previous version
-
2
@content.version = @page.content.version
-
-
2
@text = @content.text
-
2
if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
-
@section = params[:section].to_i
-
@text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
-
render_404 if @text.blank?
-
end
-
end
-
-
# Creates a new page or updates an existing one
-
1
def update
-
return render_403 unless editable?
-
was_new_page = @page.new_record?
-
@page.content = WikiContent.new(:page => @page) if @page.new_record?
-
@page.safe_attributes = params[:wiki_page]
-
-
@content = @page.content
-
content_params = params[:content]
-
if content_params.nil? && params[:wiki_page].is_a?(Hash)
-
content_params = params[:wiki_page].slice(:text, :comments, :version)
-
end
-
content_params ||= {}
-
-
@content.comments = content_params[:comments]
-
@text = content_params[:text]
-
if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
-
@section = params[:section].to_i
-
@section_hash = params[:section_hash]
-
@content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
-
else
-
@content.version = content_params[:version] if content_params[:version]
-
@content.text = @text
-
end
-
@content.author = User.current
-
-
if @page.save_with_content
-
attachments = Attachment.attach_files(@page, params[:attachments])
-
render_attachment_warning_if_needed(@page)
-
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
-
-
respond_to do |format|
-
format.html { redirect_to :action => 'show', :project_id => @project, :id => @page.title }
-
format.api {
-
if was_new_page
-
render :action => 'show', :status => :created, :location => url_for(:controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title)
-
else
-
render_api_ok
-
end
-
}
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'edit' }
-
format.api { render_validation_errors(@content) }
-
end
-
end
-
-
rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
-
# Optimistic locking exception
-
respond_to do |format|
-
format.html {
-
flash.now[:error] = l(:notice_locking_conflict)
-
render :action => 'edit'
-
}
-
format.api { render_api_head :conflict }
-
end
-
rescue ActiveRecord::RecordNotSaved
-
respond_to do |format|
-
format.html { render :action => 'edit' }
-
format.api { render_validation_errors(@content) }
-
end
-
end
-
-
# rename a page
-
1
def rename
-
return render_403 unless editable?
-
@page.redirect_existing_links = true
-
# used to display the *original* title if some AR validation errors occur
-
@original_title = @page.pretty_title
-
if request.post? && @page.update_attributes(params[:wiki_page])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'show', :project_id => @project, :id => @page.title
-
end
-
end
-
-
1
def protect
-
@page.update_attribute :protected, params[:protected]
-
redirect_to :action => 'show', :project_id => @project, :id => @page.title
-
end
-
-
# show page history
-
1
def history
-
@version_count = @page.content.versions.count
-
@version_pages = Paginator.new self, @version_count, per_page_option, params['page']
-
# don't load text
-
@versions = @page.content.versions.find :all,
-
:select => "id, author_id, comments, updated_on, version",
-
:order => 'version DESC',
-
:limit => @version_pages.items_per_page + 1,
-
:offset => @version_pages.current.offset
-
-
render :layout => false if request.xhr?
-
end
-
-
1
def diff
-
@diff = @page.diff(params[:version], params[:version_from])
-
render_404 unless @diff
-
end
-
-
1
def annotate
-
@annotate = @page.annotate(params[:version])
-
render_404 unless @annotate
-
end
-
-
# Removes a wiki page and its history
-
# Children can be either set as root pages, removed or reassigned to another parent page
-
1
def destroy
-
return render_403 unless editable?
-
-
@descendants_count = @page.descendants.size
-
if @descendants_count > 0
-
case params[:todo]
-
when 'nullify'
-
# Nothing to do
-
when 'destroy'
-
# Removes all its descendants
-
@page.descendants.each(&:destroy)
-
when 'reassign'
-
# Reassign children to another parent page
-
reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
-
return unless reassign_to
-
@page.children.each do |child|
-
child.update_attribute(:parent, reassign_to)
-
end
-
else
-
@reassignable_to = @wiki.pages - @page.self_and_descendants
-
# display the destroy form if it's a user request
-
return unless api_request?
-
end
-
end
-
@page.destroy
-
respond_to do |format|
-
format.html { redirect_to :action => 'index', :project_id => @project }
-
format.api { render_api_ok }
-
end
-
end
-
-
1
def destroy_version
-
return render_403 unless editable?
-
-
@content = @page.content_for_version(params[:version])
-
@content.destroy
-
redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project
-
end
-
-
# Export wiki to a single pdf or html file
-
1
def export
-
@pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}])
-
respond_to do |format|
-
format.html {
-
export = render_to_string :action => 'export_multiple', :layout => false
-
send_data(export, :type => 'text/html', :filename => "wiki.html")
-
}
-
format.pdf {
-
send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
-
}
-
end
-
end
-
-
1
def preview
-
page = @wiki.find_page(params[:id])
-
# page is nil when previewing a new page
-
return render_403 unless page.nil? || editable?(page)
-
if page
-
@attachements = page.attachments
-
@previewed = page.content
-
end
-
@text = params[:content][:text]
-
render :partial => 'common/preview'
-
end
-
-
1
def add_attachment
-
return render_403 unless editable?
-
attachments = Attachment.attach_files(@page, params[:attachments])
-
render_attachment_warning_if_needed(@page)
-
redirect_to :action => 'show', :id => @page.title, :project_id => @project
-
end
-
-
1
private
-
-
1
def find_wiki
-
4
@project = Project.find(params[:project_id])
-
4
@wiki = @project.wiki
-
4
render_404 unless @wiki
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# Finds the requested page or a new page if it doesn't exist
-
1
def find_existing_or_new_page
-
4
@page = @wiki.find_or_new_page(params[:id])
-
4
if @wiki.page_found_with_redirect?
-
redirect_to params.update(:id => @page.title)
-
end
-
end
-
-
# Finds the requested page and returns a 404 error if it doesn't exist
-
1
def find_existing_page
-
@page = @wiki.find_page(params[:id])
-
if @page.nil?
-
render_404
-
return
-
end
-
if @wiki.page_found_with_redirect?
-
redirect_to params.update(:id => @page.title)
-
end
-
end
-
-
# Returns true if the current user is allowed to edit the page, otherwise false
-
1
def editable?(page = @page)
-
4
page.editable_by?(User.current)
-
end
-
-
# Returns the default content of a new wiki page
-
1
def initial_page_content(page)
-
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
-
extend helper unless self.instance_of?(helper)
-
helper.instance_method(:initial_page_content).bind(self).call(page)
-
end
-
-
1
def load_pages_for_index
-
@pages = @wiki.pages.with_updated_on.order("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WikisController < ApplicationController
-
1
menu_item :settings
-
1
before_filter :find_project, :authorize
-
-
# Create or update a project's wiki
-
1
def edit
-
@wiki = @project.wiki || Wiki.new(:project => @project)
-
@wiki.safe_attributes = params[:wiki]
-
@wiki.save if request.post?
-
end
-
-
# Delete a project's wiki
-
1
def destroy
-
if request.post? && params[:confirm] && @project.wiki
-
@project.wiki.destroy
-
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki'
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WorkflowsController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin, :find_roles, :find_trackers
-
-
1
def index
-
@workflow_counts = WorkflowTransition.count_by_tracker_and_role
-
end
-
-
1
def edit
-
@role = Role.find_by_id(params[:role_id]) if params[:role_id]
-
@tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id]
-
-
if request.post?
-
WorkflowTransition.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
-
(params[:issue_status] || []).each { |status_id, transitions|
-
transitions.each { |new_status_id, options|
-
author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
-
assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
-
WorkflowTransition.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
-
}
-
}
-
if @role.save
-
redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only]
-
return
-
end
-
end
-
-
@used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
-
if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
-
@statuses = @tracker.issue_statuses
-
end
-
@statuses ||= IssueStatus.sorted.all
-
-
if @tracker && @role && @statuses.any?
-
workflows = WorkflowTransition.where(:role_id => @role.id, :tracker_id => @tracker.id).all
-
@workflows = {}
-
@workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
-
@workflows['author'] = workflows.select {|w| w.author}
-
@workflows['assignee'] = workflows.select {|w| w.assignee}
-
end
-
end
-
-
1
def permissions
-
@role = Role.find_by_id(params[:role_id]) if params[:role_id]
-
@tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id]
-
-
if request.post? && @role && @tracker
-
WorkflowPermission.replace_permissions(@tracker, @role, params[:permissions] || {})
-
redirect_to :action => 'permissions', :role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only]
-
return
-
end
-
-
@used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
-
if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
-
@statuses = @tracker.issue_statuses
-
end
-
@statuses ||= IssueStatus.sorted.all
-
-
if @role && @tracker
-
@fields = (Tracker::CORE_FIELDS_ALL - @tracker.disabled_core_fields).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
-
@custom_fields = @tracker.custom_fields
-
-
@permissions = WorkflowPermission.where(:tracker_id => @tracker.id, :role_id => @role.id).all.inject({}) do |h, w|
-
h[w.old_status_id] ||= {}
-
h[w.old_status_id][w.field_name] = w.rule
-
h
-
end
-
@statuses.each {|status| @permissions[status.id] ||= {}}
-
end
-
end
-
-
1
def copy
-
-
if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
-
@source_tracker = nil
-
else
-
@source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
-
end
-
if params[:source_role_id].blank? || params[:source_role_id] == 'any'
-
@source_role = nil
-
else
-
@source_role = Role.find_by_id(params[:source_role_id].to_i)
-
end
-
-
@target_trackers = params[:target_tracker_ids].blank? ? nil : Tracker.find_all_by_id(params[:target_tracker_ids])
-
@target_roles = params[:target_role_ids].blank? ? nil : Role.find_all_by_id(params[:target_role_ids])
-
-
if request.post?
-
if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
-
flash.now[:error] = l(:error_workflow_copy_source)
-
elsif @target_trackers.nil? || @target_roles.nil?
-
flash.now[:error] = l(:error_workflow_copy_target)
-
else
-
WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role
-
end
-
end
-
end
-
-
1
private
-
-
1
def find_roles
-
@roles = Role.sorted.all
-
end
-
-
1
def find_trackers
-
@trackers = Tracker.sorted.all
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module AccountHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module AdminHelper
-
1
def project_status_options_for_select(selected)
-
options_for_select([[l(:label_all), ''],
-
[l(:project_status_active), '1'],
-
[l(:project_status_closed), '5'],
-
[l(:project_status_archived), '9']], selected.to_s)
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'forwardable'
-
1
require 'cgi'
-
-
1
module ApplicationHelper
-
1
include Redmine::WikiFormatting::Macros::Definitions
-
1
include Redmine::I18n
-
1
include GravatarHelper::PublicMethods
-
-
1
extend Forwardable
-
1
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
-
-
# Return true if user is authorized for controller/action, otherwise false
-
1
def authorize_for(controller, action)
-
114
User.current.allowed_to?({:controller => controller, :action => action}, @project)
-
end
-
-
# Display a link if user is authorized
-
#
-
# @param [String] name Anchor text (passed to link_to)
-
# @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
-
# @param [optional, Hash] html_options Options passed to link_to
-
# @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
-
1
def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
-
45
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
-
end
-
-
# Displays a link to user's account page if active
-
1
def link_to_user(user, options={})
-
508
if user.is_a?(User)
-
503
name = h(user.name(options[:format]))
-
503
if user.active? || (User.current.admin? && user.logged?)
-
503
link_to name, user_path(user), :class => user.css_classes
-
else
-
name
-
end
-
else
-
5
h(user.to_s)
-
end
-
end
-
-
# Displays a link to +issue+ with its subject.
-
# Examples:
-
#
-
# link_to_issue(issue) # => Defect #6: This is the subject
-
# link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
-
# link_to_issue(issue, :subject => false) # => Defect #6
-
# link_to_issue(issue, :project => true) # => Foo - Defect #6
-
# link_to_issue(issue, :subject => false, :tracker => false) # => #6
-
#
-
1
def link_to_issue(issue, options={})
-
25
title = nil
-
25
subject = nil
-
25
text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
-
25
if options[:subject] == false
-
2
title = truncate(issue.subject, :length => 60)
-
else
-
23
subject = issue.subject
-
23
if options[:truncate]
-
14
subject = truncate(subject, :length => options[:truncate])
-
end
-
end
-
25
s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
-
25
s << h(": #{subject}") if subject
-
25
s = h("#{issue.project} - ") + s if options[:project]
-
25
s
-
end
-
-
# Generates a link to an attachment.
-
# Options:
-
# * :text - Link text (default to attachment filename)
-
# * :download - Force download (default: false)
-
1
def link_to_attachment(attachment, options={})
-
text = options.delete(:text) || attachment.filename
-
action = options.delete(:download) ? 'download' : 'show'
-
opt_only_path = {}
-
opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
-
options.delete(:only_path)
-
link_to(h(text),
-
{:controller => 'attachments', :action => action,
-
:id => attachment, :filename => attachment.filename}.merge(opt_only_path),
-
options)
-
end
-
-
# Generates a link to a SCM revision
-
# Options:
-
# * :text - Link text (default to the formatted revision)
-
1
def link_to_revision(revision, repository, options={})
-
if repository.is_a?(Project)
-
repository = repository.repository
-
end
-
text = options.delete(:text) || format_revision(revision)
-
rev = revision.respond_to?(:identifier) ? revision.identifier : revision
-
link_to(
-
h(text),
-
{:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
-
:title => l(:label_revision_id, format_revision(revision))
-
)
-
end
-
-
# Generates a link to a message
-
1
def link_to_message(message, options={}, html_options = nil)
-
link_to(
-
h(truncate(message.subject, :length => 60)),
-
{ :controller => 'messages', :action => 'show',
-
:board_id => message.board_id,
-
:id => (message.parent_id || message.id),
-
:r => (message.parent_id && message.id),
-
:anchor => (message.parent_id ? "message-#{message.id}" : nil)
-
}.merge(options),
-
html_options
-
)
-
end
-
-
# Generates a link to a project if active
-
# Examples:
-
#
-
# link_to_project(project) # => link to the specified project overview
-
# link_to_project(project, :action=>'settings') # => link to project settings
-
# link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
-
# link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
-
#
-
1
def link_to_project(project, options={}, html_options = nil)
-
642
if project.archived?
-
h(project)
-
else
-
642
url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
-
642
link_to(h(project), url, html_options)
-
end
-
end
-
-
1
def wiki_page_path(page, options={})
-
url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
-
end
-
-
1
def thumbnail_tag(attachment)
-
link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
-
{:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
-
:title => attachment.filename
-
end
-
-
1
def toggle_link(name, id, options={})
-
3
onclick = "$('##{id}').toggle(); "
-
3
onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
-
3
onclick << "return false;"
-
3
link_to(name, "#", :onclick => onclick)
-
end
-
-
1
def image_to_function(name, function, html_options = {})
-
html_options.symbolize_keys!
-
tag(:input, html_options.merge({
-
:type => "image", :src => image_path(name),
-
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
-
}))
-
end
-
-
1
def format_activity_title(text)
-
h(truncate_single_line(text, :length => 100))
-
end
-
-
1
def format_activity_day(date)
-
date == User.current.today ? l(:label_today).titleize : format_date(date)
-
end
-
-
1
def format_activity_description(text)
-
h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
-
).gsub(/[\r\n]+/, "<br />").html_safe
-
end
-
-
1
def format_version_name(version)
-
58
if version.project == @project
-
37
h(version)
-
else
-
21
h("#{version.project} - #{version}")
-
end
-
end
-
-
1
def due_date_distance_in_words(date)
-
13
if date
-
13
l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
-
end
-
end
-
-
# Renders a tree of projects as a nested set of unordered lists
-
# The given collection may be a subset of the whole project tree
-
# (eg. some intermediate nodes are private and can not be seen)
-
1
def render_project_nested_lists(projects)
-
s = ''
-
if projects.any?
-
ancestors = []
-
original_project = @project
-
projects.sort_by(&:lft).each do |project|
-
# set the project environment to please macros.
-
@project = project
-
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
-
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
-
else
-
ancestors.pop
-
s << "</li>"
-
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
-
ancestors.pop
-
s << "</ul></li>\n"
-
end
-
end
-
classes = (ancestors.empty? ? 'root' : 'child')
-
s << "<li class='#{classes}'><div class='#{classes}'>"
-
s << h(block_given? ? yield(project) : project.name)
-
s << "</div>\n"
-
ancestors << project
-
end
-
s << ("</li></ul>\n" * ancestors.size)
-
@project = original_project
-
end
-
s.html_safe
-
end
-
-
1
def render_page_hierarchy(pages, node=nil, options={})
-
content = ''
-
if pages[node]
-
content << "<ul class=\"pages-hierarchy\">\n"
-
pages[node].each do |page|
-
content << "<li>"
-
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
-
:title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
-
content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
-
content << "</li>\n"
-
end
-
content << "</ul>\n"
-
end
-
content.html_safe
-
end
-
-
# Renders flash messages
-
1
def render_flash_messages
-
467
s = ''
-
467
flash.each do |k,v|
-
10
s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
-
end
-
467
s.html_safe
-
end
-
-
# Renders tabs and their content
-
1
def render_tabs(tabs)
-
7
if tabs.any?
-
7
render :partial => 'common/tabs', :locals => {:tabs => tabs}
-
else
-
content_tag 'p', l(:label_no_data), :class => "nodata"
-
end
-
end
-
-
# Renders the project quick-jump box
-
1
def render_project_jump_box
-
467
return unless User.current.logged?
-
342
projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
-
342
if projects.any?
-
342
options =
-
("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
-
342
'<option value="" disabled="disabled">---</option>').html_safe
-
-
options << project_tree_options_for_select(projects, :selected => @project) do |p|
-
1112
{ :value => project_path(:id => p, :jump => current_menu_item) }
-
342
end
-
-
342
select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
-
end
-
end
-
-
1
def project_tree_options_for_select(projects, options = {})
-
347
s = ''
-
347
project_tree(projects) do |project, level|
-
1129
name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe
-
1129
tag_options = {:value => project.id}
-
1129
if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
-
221
tag_options[:selected] = 'selected'
-
else
-
908
tag_options[:selected] = nil
-
end
-
1129
tag_options.merge!(yield(project)) if block_given?
-
1129
s << content_tag('option', name_prefix + h(project), tag_options)
-
end
-
347
s.html_safe
-
end
-
-
# Yields the given block for each project with its level in the tree
-
#
-
# Wrapper for Project#project_tree
-
1
def project_tree(projects, &block)
-
347
Project.project_tree(projects, &block)
-
end
-
-
1
def principals_check_box_tags(name, principals)
-
7
s = ''
-
7
principals.sort.each do |principal|
-
49
s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
-
end
-
7
s.html_safe
-
end
-
-
# Returns a string for users/groups option tags
-
1
def principals_options_for_select(collection, selected=nil)
-
5
s = ''
-
5
if collection.include?(User.current)
-
5
s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
-
end
-
5
groups = ''
-
5
collection.sort.each do |element|
-
10
selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
-
10
(element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
-
end
-
5
unless groups.empty?
-
s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
-
end
-
5
s.html_safe
-
end
-
-
# Options for the new membership projects combo-box
-
1
def options_for_membership_project_select(principal, projects)
-
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
-
options << project_tree_options_for_select(projects) do |p|
-
{:disabled => principal.projects.include?(p)}
-
end
-
options
-
end
-
-
# Truncates and returns the string as a single line
-
1
def truncate_single_line(string, *args)
-
truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
-
end
-
-
# Truncates at line break after 250 characters or options[:length]
-
1
def truncate_lines(string, options={})
-
length = options[:length] || 250
-
if string.to_s =~ /\A(.{#{length}}.*?)$/m
-
"#{$1}..."
-
else
-
string
-
end
-
end
-
-
1
def anchor(text)
-
text.to_s.gsub(' ', '_')
-
end
-
-
1
def html_hours(text)
-
10
text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
-
end
-
-
1
def authoring(created, author, options={})
-
114
l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
-
end
-
-
1
def time_tag(time)
-
117
text = distance_of_time_in_words(Time.now, time)
-
117
if @project
-
106
link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
-
else
-
11
content_tag('acronym', text, :title => format_time(time))
-
end
-
end
-
-
1
def syntax_highlight_lines(name, content)
-
lines = []
-
syntax_highlight(name, content).each_line { |line| lines << line }
-
lines
-
end
-
-
1
def syntax_highlight(name, content)
-
Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
-
end
-
-
1
def to_path_param(path)
-
str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
-
str.blank? ? nil : str
-
end
-
-
1
def pagination_links_full(paginator, count=nil, options={})
-
17
page_param = options.delete(:page_param) || :page
-
17
per_page_links = options.delete(:per_page_links)
-
17
url_param = params.dup
-
-
17
html = ''
-
17
if paginator.current.previous
-
# \xc2\xab(utf-8) = «
-
html << link_to_content_update(
-
"\xc2\xab " + l(:label_previous),
-
url_param.merge(page_param => paginator.current.previous)) + ' '
-
end
-
-
html << (pagination_links_each(paginator, options) do |n|
-
link_to_content_update(n.to_s, url_param.merge(page_param => n))
-
17
end || '')
-
-
17
if paginator.current.next
-
# \xc2\xbb(utf-8) = »
-
html << ' ' + link_to_content_update(
-
(l(:label_next) + " \xc2\xbb"),
-
url_param.merge(page_param => paginator.current.next))
-
end
-
-
17
unless count.nil?
-
17
html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
-
17
if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
-
html << " | #{links}"
-
end
-
end
-
-
17
html.html_safe
-
end
-
-
1
def per_page_links(selected=nil, item_count=nil)
-
17
values = Setting.per_page_options_array
-
17
if item_count && values.any?
-
17
if item_count > values.first
-
max = values.detect {|value| value >= item_count} || item_count
-
else
-
17
max = item_count
-
end
-
68
values = values.select {|value| value <= max || value == selected}
-
end
-
17
if values.empty? || (values.size == 1 && values.first == selected)
-
17
return nil
-
end
-
links = values.collect do |n|
-
n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
-
end
-
l(:label_display_per_page, links.join(', '))
-
end
-
-
1
def reorder_links(name, url, method = :post)
-
link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
-
url.merge({"#{name}[move_to]" => 'highest'}),
-
:method => method, :title => l(:label_sort_highest)) +
-
link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
-
url.merge({"#{name}[move_to]" => 'higher'}),
-
:method => method, :title => l(:label_sort_higher)) +
-
link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
-
url.merge({"#{name}[move_to]" => 'lower'}),
-
:method => method, :title => l(:label_sort_lower)) +
-
link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
-
url.merge({"#{name}[move_to]" => 'lowest'}),
-
14
:method => method, :title => l(:label_sort_lowest))
-
end
-
-
1
def breadcrumb(*args)
-
8
elements = args.flatten
-
8
elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
-
end
-
-
1
def other_formats_links(&block)
-
30
concat('<p class="other-formats">'.html_safe + l(:label_export_to))
-
30
yield Redmine::Views::OtherFormatsBuilder.new(self)
-
30
concat('</p>'.html_safe)
-
end
-
-
1
def page_header_title
-
374
if @project.nil? || @project.new_record?
-
251
h(Setting.app_title)
-
else
-
123
b = []
-
123
ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
-
123
if ancestors.any?
-
6
root = ancestors.shift
-
6
b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
-
6
if ancestors.size > 2
-
b << "\xe2\x80\xa6"
-
ancestors = ancestors[-2, 2]
-
end
-
6
b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
-
end
-
123
b << h(@project)
-
123
b.join(" \xc2\xbb ").html_safe
-
end
-
end
-
-
1
def html_title(*args)
-
703
if args.empty?
-
467
title = @html_title || []
-
467
title << @project.name if @project
-
467
title << Setting.app_title unless Setting.app_title == title.last
-
1388
title.select {|t| !t.blank? }.join(' - ')
-
else
-
236
@html_title ||= []
-
236
@html_title += args
-
end
-
end
-
-
# Returns the theme, controller name, and action as css classes for the
-
# HTML body.
-
1
def body_css_classes
-
374
css = []
-
374
if theme = Redmine::Themes.theme(Setting.ui_theme)
-
css << 'theme-' + theme.name
-
end
-
-
374
css << 'controller-' + controller_name
-
374
css << 'action-' + action_name
-
374
css.join(' ')
-
end
-
-
1
def accesskey(s)
-
776
Redmine::AccessKeys.key_for s
-
end
-
-
# Formats text according to system settings.
-
# 2 ways to call this method:
-
# * with a String: textilizable(text, options)
-
# * with an object and one of its attribute: textilizable(issue, :description, options)
-
1
def textilizable(*args)
-
1333
options = args.last.is_a?(Hash) ? args.pop : {}
-
1333
case args.size
-
when 1
-
81
obj = options[:object]
-
81
text = args.shift
-
when 2
-
1252
obj = args.shift
-
1252
attr = args.shift
-
1252
text = obj.send(attr).to_s
-
else
-
raise ArgumentError, 'invalid arguments to textilizable'
-
end
-
1333
return '' if text.blank?
-
79
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
-
79
only_path = options.delete(:only_path) == false ? false : true
-
-
79
text = text.dup
-
79
macros = catch_macros(text)
-
79
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
-
-
79
@parsed_headings = []
-
79
@heading_anchors = {}
-
79
@current_section = 0 if options[:edit_section_links]
-
-
79
parse_sections(text, project, obj, attr, only_path, options)
-
79
text = parse_non_pre_blocks(text, obj, macros) do |text|
-
79
[:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
-
237
send method_name, text, project, obj, attr, only_path, options
-
end
-
end
-
79
parse_headings(text, project, obj, attr, only_path, options)
-
-
79
if @parsed_headings.any?
-
3
replace_toc(text, @parsed_headings)
-
end
-
-
79
text.html_safe
-
end
-
-
1
def parse_non_pre_blocks(text, obj, macros)
-
79
s = StringScanner.new(text)
-
79
tags = []
-
79
parsed = ''
-
79
while !s.eos?
-
79
s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
-
79
text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
-
79
if tags.empty?
-
79
yield text
-
79
inject_macros(text, obj, macros) if macros.any?
-
else
-
inject_macros(text, obj, macros, false) if macros.any?
-
end
-
79
parsed << text
-
79
if tag
-
if closing
-
if tags.last == tag.downcase
-
tags.pop
-
end
-
else
-
tags << tag.downcase
-
end
-
parsed << full_tag
-
end
-
end
-
# Close any non closing tags
-
79
while tag = tags.pop
-
parsed << "</#{tag}>"
-
end
-
79
parsed
-
end
-
-
1
def parse_inline_attachments(text, project, obj, attr, only_path, options)
-
# when using an image link, try to use an attachment, if possible
-
79
attachments = options[:attachments] || []
-
79
attachments += obj.attachments if obj.respond_to?(:attachments)
-
79
if attachments.present?
-
text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
-
filename, ext, alt, alttext = $1.downcase, $2, $3, $4
-
# search for the picture in attachments
-
if found = Attachment.latest_attach(attachments, filename)
-
image_url = url_for :only_path => only_path, :controller => 'attachments',
-
:action => 'download', :id => found
-
desc = found.description.to_s.gsub('"', '')
-
if !desc.blank? && alttext.blank?
-
alt = " title=\"#{desc}\" alt=\"#{desc}\""
-
end
-
"src=\"#{image_url}\"#{alt}"
-
else
-
m
-
end
-
end
-
end
-
end
-
-
# Wiki links
-
#
-
# Examples:
-
# [[mypage]]
-
# [[mypage|mytext]]
-
# wiki links can refer other project wikis, using project name or identifier:
-
# [[project:]] -> wiki starting page
-
# [[project:|mytext]]
-
# [[project:mypage]]
-
# [[project:mypage|mytext]]
-
1
def parse_wiki_links(text, project, obj, attr, only_path, options)
-
79
text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
-
link_project = project
-
esc, all, page, title = $1, $2, $3, $5
-
if esc.nil?
-
if page =~ /^([^\:]+)\:(.*)$/
-
link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
-
page = $2
-
title ||= $1 if page.blank?
-
end
-
-
if link_project && link_project.wiki
-
# extract anchor
-
anchor = nil
-
if page =~ /^(.+?)\#(.+)$/
-
page, anchor = $1, $2
-
end
-
anchor = sanitize_anchor_name(anchor) if anchor.present?
-
# check if page exists
-
wiki_page = link_project.wiki.find_page(page)
-
url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
-
"##{anchor}"
-
else
-
case options[:wiki_links]
-
when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
-
when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
-
else
-
wiki_page_id = page.present? ? Wiki.titleize(page) : nil
-
parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
-
url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
-
:id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
-
end
-
end
-
link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
-
else
-
# project or wiki doesn't exist
-
all
-
end
-
else
-
all
-
end
-
end
-
end
-
-
# Redmine links
-
#
-
# Examples:
-
# Issues:
-
# #52 -> Link to issue #52
-
# Changesets:
-
# r52 -> Link to revision 52
-
# commit:a85130f -> Link to scmid starting with a85130f
-
# Documents:
-
# document#17 -> Link to document with id 17
-
# document:Greetings -> Link to the document with title "Greetings"
-
# document:"Some document" -> Link to the document with title "Some document"
-
# Versions:
-
# version#3 -> Link to version with id 3
-
# version:1.0.0 -> Link to version named "1.0.0"
-
# version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
-
# Attachments:
-
# attachment:file.zip -> Link to the attachment of the current object named file.zip
-
# Source files:
-
# source:some/file -> Link to the file located at /some/file in the project's repository
-
# source:some/file@52 -> Link to the file's revision 52
-
# source:some/file#L120 -> Link to line 120 of the file
-
# source:some/file@52#L120 -> Link to line 120 of the file's revision 52
-
# export:some/file -> Force the download of the file
-
# Forum messages:
-
# message#1218 -> Link to message with id 1218
-
#
-
# Links can refer other objects from other projects, using project identifier:
-
# identifier:r52
-
# identifier:document:"Some document"
-
# identifier:version:1.0.0
-
# identifier:source:some/file
-
1
def parse_redmine_links(text, default_project, obj, attr, only_path, options)
-
79
text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
-
leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
-
link = nil
-
project = default_project
-
if project_identifier
-
project = Project.visible.find_by_identifier(project_identifier)
-
end
-
if esc.nil?
-
if prefix.nil? && sep == 'r'
-
if project
-
repository = nil
-
if repo_identifier
-
repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
-
else
-
repository = project.repository
-
end
-
# project.changesets.visible raises an SQL error because of a double join on repositories
-
if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
-
link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
-
:class => 'changeset',
-
:title => truncate_single_line(changeset.comments, :length => 100))
-
end
-
end
-
elsif sep == '#'
-
oid = identifier.to_i
-
case prefix
-
when nil
-
if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
-
anchor = comment_id ? "note-#{comment_id}" : nil
-
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
-
:class => issue.css_classes,
-
:title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
-
end
-
when 'document'
-
if document = Document.visible.find_by_id(oid)
-
link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
-
:class => 'document'
-
end
-
when 'version'
-
if version = Version.visible.find_by_id(oid)
-
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
-
:class => 'version'
-
end
-
when 'message'
-
if message = Message.visible.find_by_id(oid, :include => :parent)
-
link = link_to_message(message, {:only_path => only_path}, :class => 'message')
-
end
-
when 'forum'
-
if board = Board.visible.find_by_id(oid)
-
link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
-
:class => 'board'
-
end
-
when 'news'
-
if news = News.visible.find_by_id(oid)
-
link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
-
:class => 'news'
-
end
-
when 'project'
-
if p = Project.visible.find_by_id(oid)
-
link = link_to_project(p, {:only_path => only_path}, :class => 'project')
-
end
-
end
-
elsif sep == ':'
-
# removes the double quotes if any
-
name = identifier.gsub(%r{^"(.*)"$}, "\\1")
-
case prefix
-
when 'document'
-
if project && document = project.documents.visible.find_by_title(name)
-
link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
-
:class => 'document'
-
end
-
when 'version'
-
if project && version = project.versions.visible.find_by_name(name)
-
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
-
:class => 'version'
-
end
-
when 'forum'
-
if project && board = project.boards.visible.find_by_name(name)
-
link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
-
:class => 'board'
-
end
-
when 'news'
-
if project && news = project.news.visible.find_by_title(name)
-
link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
-
:class => 'news'
-
end
-
when 'commit', 'source', 'export'
-
if project
-
repository = nil
-
if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
-
repo_prefix, repo_identifier, name = $1, $2, $3
-
repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
-
else
-
repository = project.repository
-
end
-
if prefix == 'commit'
-
if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
-
link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
-
:class => 'changeset',
-
:title => truncate_single_line(h(changeset.comments), :length => 100)
-
end
-
else
-
if repository && User.current.allowed_to?(:browse_repository, project)
-
name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
-
path, rev, anchor = $1, $3, $5
-
link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
-
:path => to_path_param(path),
-
:rev => rev,
-
:anchor => anchor},
-
:class => (prefix == 'export' ? 'source download' : 'source')
-
end
-
end
-
repo_prefix = nil
-
end
-
when 'attachment'
-
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
-
if attachments && attachment = Attachment.latest_attach(attachments, name)
-
link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
-
:class => 'attachment'
-
end
-
when 'project'
-
if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
-
link = link_to_project(p, {:only_path => only_path}, :class => 'project')
-
end
-
end
-
end
-
end
-
(leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
-
end
-
end
-
-
1
HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
-
-
1
def parse_sections(text, project, obj, attr, only_path, options)
-
79
return unless options[:edit_section_links]
-
2
text.gsub!(HEADING_RE) do
-
2
heading = $1
-
2
@current_section += 1
-
2
if @current_section > 1
-
content_tag('div',
-
link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
-
:class => 'contextual',
-
:title => l(:button_edit_section)) + heading.html_safe
-
else
-
2
heading
-
end
-
end
-
end
-
-
# Headings and TOC
-
# Adds ids and links to headings unless options[:headings] is set to false
-
1
def parse_headings(text, project, obj, attr, only_path, options)
-
79
return if options[:headings] == false
-
-
79
text.gsub!(HEADING_RE) do
-
3
level, attrs, content = $2.to_i, $3, $4
-
3
item = strip_tags(content).strip
-
3
anchor = sanitize_anchor_name(item)
-
# used for single-file wiki export
-
3
anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
-
3
@heading_anchors[anchor] ||= 0
-
3
idx = (@heading_anchors[anchor] += 1)
-
3
if idx > 1
-
anchor = "#{anchor}-#{idx}"
-
end
-
3
@parsed_headings << [level, anchor, item]
-
3
"<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a></h#{level}>"
-
end
-
end
-
-
MACROS_RE = /(
-
(!)? # escaping
-
(
-
\{\{ # opening tag
-
([\w]+) # macro name
-
(\(([^\n\r]*?)\))? # optional arguments
-
([\n\r].*?[\n\r])? # optional block of text
-
\}\} # closing tag
-
)
-
1
)/mx unless const_defined?(:MACROS_RE)
-
-
MACRO_SUB_RE = /(
-
\{\{
-
macro\((\d+)\)
-
\}\}
-
1
)/x unless const_defined?(:MACRO_SUB_RE)
-
-
# Extracts macros from text
-
1
def catch_macros(text)
-
79
macros = {}
-
79
text.gsub!(MACROS_RE) do
-
all, macro = $1, $4.downcase
-
if macro_exists?(macro) || all =~ MACRO_SUB_RE
-
index = macros.size
-
macros[index] = all
-
"{{macro(#{index})}}"
-
else
-
all
-
end
-
end
-
79
macros
-
end
-
-
# Executes and replaces macros in text
-
1
def inject_macros(text, obj, macros, execute=true)
-
text.gsub!(MACRO_SUB_RE) do
-
all, index = $1, $2.to_i
-
orig = macros.delete(index)
-
if execute && orig && orig =~ MACROS_RE
-
esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
-
if esc.nil?
-
h(exec_macro(macro, obj, args, block) || all)
-
else
-
h(all)
-
end
-
elsif orig
-
h(orig)
-
else
-
h(all)
-
end
-
end
-
end
-
-
1
TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
-
-
# Renders the TOC with given headings
-
1
def replace_toc(text, headings)
-
3
text.gsub!(TOC_RE) do
-
# Keep only the 4 first levels
-
headings = headings.select{|level, anchor, item| level <= 4}
-
if headings.empty?
-
''
-
else
-
div_class = 'toc'
-
div_class << ' right' if $1 == '>'
-
div_class << ' left' if $1 == '<'
-
out = "<ul class=\"#{div_class}\"><li>"
-
root = headings.map(&:first).min
-
current = root
-
started = false
-
headings.each do |level, anchor, item|
-
if level > current
-
out << '<ul><li>' * (level - current)
-
elsif level < current
-
out << "</li></ul>\n" * (current - level) + "</li><li>"
-
elsif started
-
out << '</li><li>'
-
end
-
out << "<a href=\"##{anchor}\">#{item}</a>"
-
current = level
-
started = true
-
end
-
out << '</li></ul>' * (current - root)
-
out << '</li></ul>'
-
end
-
end
-
end
-
-
# Same as Rails' simple_format helper without using paragraphs
-
1
def simple_format_without_paragraph(text)
-
text.to_s.
-
gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
-
gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
-
gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
-
html_safe
-
end
-
-
1
def lang_options_for_select(blank=true)
-
(blank ? [["(auto)", ""]] : []) + languages_options
-
end
-
-
1
def label_tag_for(name, option_tags = nil, options = {})
-
label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
-
content_tag("label", label_text)
-
end
-
-
1
def labelled_form_for(*args, &proc)
-
17
args << {} unless args.last.is_a?(Hash)
-
17
options = args.last
-
17
if args.first.is_a?(Symbol)
-
2
options.merge!(:as => args.shift)
-
end
-
17
options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
-
17
form_for(*args, &proc)
-
end
-
-
1
def labelled_fields_for(*args, &proc)
-
16
args << {} unless args.last.is_a?(Hash)
-
16
options = args.last
-
16
options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
-
16
fields_for(*args, &proc)
-
end
-
-
1
def labelled_remote_form_for(*args, &proc)
-
ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
-
args << {} unless args.last.is_a?(Hash)
-
options = args.last
-
options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
-
form_for(*args, &proc)
-
end
-
-
1
def error_messages_for(*objects)
-
51
html = ""
-
105
objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
-
96
errors = objects.map {|o| o.errors.full_messages}.flatten
-
51
if errors.any?
-
html << "<div id='errorExplanation'><ul>\n"
-
errors.each do |error|
-
html << "<li>#{h error}</li>\n"
-
end
-
html << "</ul></div>\n"
-
end
-
51
html.html_safe
-
end
-
-
1
def delete_link(url, options={})
-
72
options = {
-
:method => :delete,
-
:data => {:confirm => l(:text_are_you_sure)},
-
:class => 'icon icon-del'
-
}.merge(options)
-
-
72
link_to l(:button_delete), url, options
-
end
-
-
1
def preview_link(url, form, target='preview', options={})
-
7
content_tag 'a', l(:label_preview), {
-
:href => "#",
-
:onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
-
:accesskey => accesskey(:preview)
-
}.merge(options)
-
end
-
-
1
def link_to_function(name, function, html_options={})
-
93
content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
-
end
-
-
# Helper to render JSON in views
-
1
def raw_json(arg)
-
136
arg.to_json.to_s.gsub('/', '\/').html_safe
-
end
-
-
1
def back_url
-
123
url = params[:back_url]
-
123
if url.nil? && referer = request.env['HTTP_REFERER']
-
2
url = CGI.unescape(referer.to_s)
-
end
-
123
url
-
end
-
-
1
def back_url_hidden_field_tag
-
123
url = back_url
-
123
hidden_field_tag('back_url', url, :id => nil) unless url.blank?
-
end
-
-
1
def check_all_links(form_name)
-
link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
-
" | ".html_safe +
-
7
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
-
end
-
-
1
def progress_bar(pcts, options={})
-
11
pcts = [pcts, pcts] unless pcts.is_a?(Array)
-
11
pcts = pcts.collect(&:round)
-
11
pcts[1] = pcts[1] - pcts[0]
-
11
pcts << (100 - pcts[1] - pcts[0])
-
11
width = options[:width] || '100px;'
-
11
legend = options[:legend] || ''
-
content_tag('table',
-
content_tag('tr',
-
11
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
-
11
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
-
11
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
-
), :class => 'progress', :style => "width: #{width};").html_safe +
-
11
content_tag('p', legend, :class => 'pourcent').html_safe
-
end
-
-
1
def checked_image(checked=true)
-
35
if checked
-
35
image_tag 'toggle_check.png'
-
end
-
end
-
-
1
def context_menu(url)
-
162
unless @context_menu_included
-
150
content_for :header_tags do
-
javascript_include_tag('context_menu') +
-
150
stylesheet_link_tag('context_menu')
-
end
-
150
if l(:direction) == 'rtl'
-
content_for :header_tags do
-
stylesheet_link_tag('context_menu_rtl')
-
end
-
end
-
150
@context_menu_included = true
-
end
-
162
javascript_tag "contextMenuInit('#{ url_for(url) }')"
-
end
-
-
1
def calendar_for(field_id)
-
20
include_calendar_headers_tags
-
20
javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
-
end
-
-
1
def include_calendar_headers_tags
-
41
unless @calendar_headers_tags_included
-
30
@calendar_headers_tags_included = true
-
30
content_for :header_tags do
-
30
start_of_week = Setting.start_of_week
-
30
start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
-
# Redmine uses 1..7 (monday..sunday) in settings and locales
-
# JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
-
30
start_of_week = start_of_week.to_i % 7
-
-
30
tags = javascript_tag(
-
"var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
-
"showOn: 'button', buttonImageOnly: true, buttonImage: '" +
-
path_to_image('/images/calendar.png') +
-
"', showButtonPanel: true};")
-
30
jquery_locale = l('jquery.locale', :default => current_language.to_s)
-
30
unless jquery_locale == 'en'
-
tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
-
end
-
30
tags
-
end
-
end
-
end
-
-
# Overrides Rails' stylesheet_link_tag with themes and plugins support.
-
# Examples:
-
# stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
-
# stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
-
#
-
1
def stylesheet_link_tag(*sources)
-
1564
options = sources.last.is_a?(Hash) ? sources.pop : {}
-
1564
plugin = options.delete(:plugin)
-
1564
sources = sources.map do |source|
-
2031
if plugin
-
1119
"/plugin_assets/#{plugin}/stylesheets/#{source}"
-
elsif current_theme && current_theme.stylesheets.include?(source)
-
current_theme.stylesheet_path(source)
-
else
-
912
source
-
end
-
end
-
1564
super sources, options
-
end
-
-
# Overrides Rails' image_tag with themes and plugins support.
-
# Examples:
-
# image_tag('image.png') # => picks image.png from the current theme or defaults
-
# image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
-
#
-
1
def image_tag(source, options={})
-
212
if plugin = options.delete(:plugin)
-
source = "/plugin_assets/#{plugin}/images/#{source}"
-
elsif current_theme && current_theme.images.include?(source)
-
source = current_theme.image_path(source)
-
end
-
212
super source, options
-
end
-
-
# Overrides Rails' javascript_include_tag with plugins support
-
# Examples:
-
# javascript_include_tag('scripts') # => picks scripts.js from defaults
-
# javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
-
#
-
1
def javascript_include_tag(*sources)
-
3210
options = sources.last.is_a?(Hash) ? sources.pop : {}
-
3210
if plugin = options.delete(:plugin)
-
2642
sources = sources.map do |source|
-
6562
if plugin
-
6562
"/plugin_assets/#{plugin}/javascripts/#{source}"
-
else
-
source
-
end
-
end
-
end
-
3210
super sources, options
-
end
-
-
1
def content_for(name, content = nil, &block)
-
1150
@has_content ||= {}
-
1150
@has_content[name] = true
-
1150
super(name, content, &block)
-
end
-
-
1
def has_content?(name)
-
467
(@has_content && @has_content[name]) || false
-
end
-
-
1
def sidebar_content?
-
467
has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
-
end
-
-
1
def view_layouts_base_sidebar_hook_response
-
834
@view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
-
end
-
-
1
def email_delivery_enabled?
-
!!ActionMailer::Base.perform_deliveries
-
end
-
-
# Returns the avatar image tag for the given +user+ if avatars are enabled
-
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
-
1
def avatar(user, options = { })
-
6
if Setting.gravatar_enabled?
-
options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
-
email = nil
-
if user.respond_to?(:mail)
-
email = user.mail
-
elsif user.to_s =~ %r{<(.+?)>}
-
email = $1
-
end
-
return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
-
else
-
6
''
-
end
-
end
-
-
1
def sanitize_anchor_name(anchor)
-
3
if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
-
3
anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
-
else
-
# TODO: remove when ruby1.8 is no longer supported
-
anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
-
end
-
end
-
-
# Returns the javascript tags that are included in the html layout head
-
1
def javascript_heads
-
374
tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
-
374
unless User.current.pref.warn_on_leaving_unsaved == '0'
-
374
tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
-
end
-
374
tags
-
end
-
-
1
def favicon
-
467
"<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
-
end
-
-
1
def robot_exclusion_tag
-
4
'<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
-
end
-
-
# Returns true if arg is expected in the API response
-
1
def include_in_api_response?(arg)
-
unless @included_in_api_response
-
param = params[:include]
-
@included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
-
@included_in_api_response.collect!(&:strip)
-
end
-
@included_in_api_response.include?(arg.to_s)
-
end
-
-
# Returns options or nil if nometa param or X-Redmine-Nometa header
-
# was set in the request
-
1
def api_meta(options)
-
if params[:nometa].present? || request.headers['X-Redmine-Nometa']
-
# compatibility mode for activeresource clients that raise
-
# an error when unserializing an array with attributes
-
nil
-
else
-
options
-
end
-
end
-
-
1
private
-
-
1
def wiki_helper
-
14
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
-
14
extend helper
-
14
return self
-
end
-
-
1
def link_to_content_update(text, url_params = {}, html_options = {})
-
123
link_to(text, url_params, html_options)
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module AttachmentsHelper
-
# Displays view/delete links to the attachments of the given object
-
# Options:
-
# :author -- author names are not displayed if set to false
-
# :thumbails -- display thumbnails if enabled in settings
-
1
def link_to_attachments(container, options = {})
-
2
options.assert_valid_keys(:author, :thumbnails)
-
-
2
if container.attachments.any?
-
options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
-
render :partial => 'attachments/links',
-
:locals => {:attachments => container.attachments, :options => options, :thumbnails => (options[:thumbnails] && Setting.thumbnails_enabled?)}
-
end
-
end
-
-
1
def render_api_attachment(attachment, api)
-
api.attachment do
-
api.id attachment.id
-
api.filename attachment.filename
-
api.filesize attachment.filesize
-
api.content_type attachment.content_type
-
api.description attachment.description
-
api.content_url url_for(:controller => 'attachments', :action => 'download', :id => attachment, :filename => attachment.filename, :only_path => false)
-
api.author(:id => attachment.author.id, :name => attachment.author.name) if attachment.author
-
api.created_on attachment.created_on
-
end
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module AuthSourcesHelper
-
1
def auth_source_partial_name(auth_source)
-
"form_#{auth_source.class.name.underscore}"
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module BoardsHelper
-
1
def board_breadcrumb(item)
-
board = item.is_a?(Message) ? item.board : item
-
links = [link_to(l(:label_board_plural), project_boards_path(item.project))]
-
boards = board.ancestors.reverse
-
if item.is_a?(Message)
-
boards << board
-
end
-
links += boards.map {|ancestor| link_to(h(ancestor.name), project_board_path(ancestor.project, ancestor))}
-
breadcrumb links
-
end
-
-
1
def boards_options_for_select(boards)
-
options = []
-
Board.board_tree(boards) do |board, level|
-
label = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe
-
label << board.name
-
options << [label, board.id]
-
end
-
options
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module CalendarsHelper
-
1
def link_to_previous_month(year, month, options={})
-
target_year, target_month = if month == 1
-
[year - 1, 12]
-
else
-
[year, month - 1]
-
end
-
-
name = if target_month == 12
-
"#{month_name(target_month)} #{target_year}"
-
else
-
"#{month_name(target_month)}"
-
end
-
-
# \xc2\xab(utf-8) = «
-
link_to_month(("\xc2\xab " + name), target_year, target_month, options)
-
end
-
-
1
def link_to_next_month(year, month, options={})
-
target_year, target_month = if month == 12
-
[year + 1, 1]
-
else
-
[year, month + 1]
-
end
-
-
name = if target_month == 1
-
"#{month_name(target_month)} #{target_year}"
-
else
-
"#{month_name(target_month)}"
-
end
-
-
# \xc2\xbb(utf-8) = »
-
link_to_month((name + " \xc2\xbb"), target_year, target_month, options)
-
end
-
-
1
def link_to_month(link_name, year, month, options={})
-
link_to_content_update(h(link_name), params.merge(:year => year, :month => month))
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module ContextMenusHelper
-
1
def context_menu_link(name, url, options={})
-
options[:class] ||= ''
-
if options.delete(:selected)
-
options[:class] << ' icon-checked disabled'
-
options[:disabled] = true
-
end
-
if options.delete(:disabled)
-
options.delete(:method)
-
options.delete(:data)
-
options[:onclick] = 'return false;'
-
options[:class] << ' disabled'
-
url = '#'
-
end
-
link_to h(name), url, options
-
end
-
-
1
def bulk_update_custom_field_context_menu_link(field, text, value)
-
context_menu_link h(text),
-
{:controller => 'issues', :action => 'bulk_update', :ids => @issue_ids, :issue => {'custom_field_values' => {field.id => value}}, :back_url => @back},
-
:method => :post,
-
:selected => (@issue && @issue.custom_field_value(field) == value)
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module CustomFieldsHelper
-
-
1
def custom_fields_tabs
-
CustomField::CUSTOM_FIELDS_TABS
-
end
-
-
# Return custom field html tag corresponding to its format
-
1
def custom_field_tag(name, custom_value)
-
40
custom_field = custom_value.custom_field
-
40
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
-
40
field_name << "[]" if custom_field.multiple?
-
40
field_id = "#{name}_custom_field_values_#{custom_field.id}"
-
-
40
tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
-
-
40
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
-
40
case field_format.try(:edit_as)
-
when "date"
-
text_field_tag(field_name, custom_value.value, tag_options.merge(:size => 10)) +
-
calendar_for(field_id)
-
when "text"
-
text_area_tag(field_name, custom_value.value, tag_options.merge(:rows => 3))
-
when "bool"
-
33
hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, tag_options)
-
when "list"
-
7
blank_option = ''.html_safe
-
7
unless custom_field.multiple?
-
7
if custom_field.is_required?
-
unless custom_field.default_value.present?
-
blank_option = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
-
end
-
else
-
7
blank_option = content_tag('option')
-
end
-
end
-
7
s = select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value),
-
tag_options.merge(:multiple => custom_field.multiple?))
-
7
if custom_field.multiple?
-
s << hidden_field_tag(field_name, '')
-
end
-
7
s
-
else
-
text_field_tag(field_name, custom_value.value, tag_options)
-
end
-
end
-
-
# Return custom field label tag
-
1
def custom_field_label_tag(name, custom_value, options={})
-
12
required = options[:required] || custom_value.custom_field.is_required?
-
-
12
content_tag "label", h(custom_value.custom_field.name) +
-
12
(required ? " <span class=\"required\">*</span>".html_safe : ""),
-
:for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
-
end
-
-
# Return custom field tag with its label tag
-
1
def custom_field_tag_with_label(name, custom_value, options={})
-
12
custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
-
end
-
-
1
def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil)
-
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
-
field_name << "[]" if custom_field.multiple?
-
field_id = "#{name}_custom_field_values_#{custom_field.id}"
-
-
tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
-
-
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
-
case field_format.try(:edit_as)
-
when "date"
-
text_field_tag(field_name, '', tag_options.merge(:size => 10)) +
-
calendar_for(field_id)
-
when "text"
-
text_area_tag(field_name, '', tag_options.merge(:rows => 3))
-
when "bool"
-
select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
-
[l(:general_text_yes), '1'],
-
[l(:general_text_no), '0']]), tag_options)
-
when "list"
-
options = []
-
options << [l(:label_no_change_option), ''] unless custom_field.multiple?
-
options << [l(:label_none), '__none__'] unless custom_field.is_required?
-
options += custom_field.possible_values_options(projects)
-
select_tag(field_name, options_for_select(options), tag_options.merge(:multiple => custom_field.multiple?))
-
else
-
text_field_tag(field_name, '', tag_options)
-
end
-
end
-
-
# Return a string used to display a custom value
-
1
def show_value(custom_value)
-
50
return "" unless custom_value
-
50
format_value(custom_value.value, custom_value.custom_field.field_format)
-
end
-
-
# Return a string used to display a custom value
-
1
def format_value(value, field_format)
-
50
if value.is_a?(Array)
-
value.collect {|v| format_value(v, field_format)}.compact.sort.join(', ')
-
else
-
50
Redmine::CustomFieldFormat.format_value(value, field_format)
-
end
-
end
-
-
# Return an array of custom field formats which can be used in select_tag
-
1
def custom_field_formats_for_select(custom_field)
-
Redmine::CustomFieldFormat.as_select(custom_field.class.customized_class.name)
-
end
-
-
# Renders the custom_values in api views
-
1
def render_api_custom_values(custom_values, api)
-
api.array :custom_fields do
-
custom_values.each do |custom_value|
-
attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
-
attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
-
api.custom_field attrs do
-
if custom_value.value.is_a?(Array)
-
api.array :value do
-
custom_value.value.each do |value|
-
api.value value unless value.blank?
-
end
-
end
-
else
-
api.value custom_value.value
-
end
-
end
-
end
-
end unless custom_values.empty?
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module DocumentsHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module EnumerationsHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module GanttHelper
-
-
1
def gantt_zoom_link(gantt, in_or_out)
-
case in_or_out
-
when :in
-
if gantt.zoom < 4
-
link_to_content_update l(:text_zoom_in),
-
params.merge(gantt.params.merge(:zoom => (gantt.zoom+1))),
-
:class => 'icon icon-zoom-in'
-
else
-
content_tag('span', l(:text_zoom_in), :class => 'icon icon-zoom-in').html_safe
-
end
-
-
when :out
-
if gantt.zoom > 1
-
link_to_content_update l(:text_zoom_out),
-
params.merge(gantt.params.merge(:zoom => (gantt.zoom-1))),
-
:class => 'icon icon-zoom-out'
-
else
-
content_tag('span', l(:text_zoom_out), :class => 'icon icon-zoom-out').html_safe
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module GroupsHelper
-
1
def group_settings_tabs
-
tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
-
{:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
-
{:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
-
]
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module IssueCategoriesHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module IssueRelationsHelper
-
1
def collection_for_relation_type_select
-
3
values = IssueRelation::TYPES
-
78
values.keys.sort{|x,y| values[x][:order] <=> values[y][:order]}.collect{|k| [l(values[k][:name]), k]}
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module IssueStatusesHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module IssuesHelper
-
1
include ApplicationHelper
-
-
1
def issue_list(issues, &block)
-
17
ancestors = []
-
17
issues.each do |issue|
-
103
while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
-
2
ancestors.pop
-
end
-
103
yield issue, ancestors.size
-
103
ancestors << issue unless issue.leaf?
-
end
-
end
-
-
# Renders a HTML/CSS tooltip
-
#
-
# To use, a trigger div is needed. This is a div with the class of "tooltip"
-
# that contains this method wrapped in a span with the class of "tip"
-
#
-
# <div class="tooltip"><%= link_to_issue(issue) %>
-
# <span class="tip"><%= render_issue_tooltip(issue) %></span>
-
# </div>
-
#
-
1
def render_issue_tooltip(issue)
-
6
@cached_label_status ||= l(:field_status)
-
6
@cached_label_start_date ||= l(:field_start_date)
-
6
@cached_label_due_date ||= l(:field_due_date)
-
6
@cached_label_assigned_to ||= l(:field_assigned_to)
-
6
@cached_label_priority ||= l(:field_priority)
-
6
@cached_label_project ||= l(:field_project)
-
-
link_to_issue(issue) + "<br /><br />".html_safe +
-
"<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
-
"<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
-
"<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
-
"<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
-
"<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
-
6
"<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
-
end
-
-
1
def issue_heading(issue)
-
3
h("#{issue.tracker} ##{issue.id}")
-
end
-
-
1
def render_issue_subject_with_tree(issue)
-
3
s = ''
-
3
ancestors = issue.root? ? [] : issue.ancestors.visible.all
-
3
ancestors.each do |ancestor|
-
s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
-
end
-
3
s << '<div>'
-
3
subject = h(issue.subject)
-
3
if issue.is_private?
-
subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
-
end
-
3
s << content_tag('h3', subject)
-
3
s << '</div>' * (ancestors.size + 1)
-
3
s.html_safe
-
end
-
-
1
def render_descendants_tree(issue)
-
3
s = '<form><table class="list issues">'
-
3
issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
-
5
css = "issue issue-#{child.id} hascontextmenu"
-
5
css << " idnt idnt-#{level}" if level > 0
-
s << content_tag('tr',
-
content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
-
5
content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
-
content_tag('td', h(child.status)) +
-
content_tag('td', link_to_user(child.assigned_to)) +
-
content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
-
5
:class => css)
-
end
-
3
s << '</table></form>'
-
3
s.html_safe
-
end
-
-
# Returns a link for adding a new subtask to the given issue
-
1
def link_to_new_subtask(issue)
-
3
attrs = {
-
:tracker_id => issue.tracker,
-
:parent_issue_id => issue
-
}
-
3
link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
-
end
-
-
1
class IssueFieldsRows
-
1
include ActionView::Helpers::TagHelper
-
-
1
def initialize
-
3
@left = []
-
3
@right = []
-
end
-
-
1
def left(*args)
-
15
args.any? ? @left << cells(*args) : @left
-
end
-
-
1
def right(*args)
-
13
args.any? ? @right << cells(*args) : @right
-
end
-
-
1
def size
-
3
@left.size > @right.size ? @left.size : @right.size
-
end
-
-
1
def to_html
-
3
html = ''.html_safe
-
3
blank = content_tag('th', '') + content_tag('td', '')
-
3
size.times do |i|
-
15
left = @left[i] || blank
-
15
right = @right[i] || blank
-
15
html << content_tag('tr', left + right)
-
end
-
3
html
-
end
-
-
1
def cells(label, text, options={})
-
28
content_tag('th', "#{label}:", options) + content_tag('td', text, options)
-
end
-
end
-
-
1
def issue_fields_rows
-
3
r = IssueFieldsRows.new
-
3
yield r
-
3
r.to_html
-
end
-
-
1
def render_custom_fields_rows(issue)
-
3
return if issue.custom_field_values.empty?
-
ordered_values = []
-
half = (issue.custom_field_values.size / 2.0).ceil
-
half.times do |i|
-
ordered_values << issue.custom_field_values[i]
-
ordered_values << issue.custom_field_values[i + half]
-
end
-
s = "<tr>\n"
-
n = 0
-
ordered_values.compact.each do |value|
-
s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
-
s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
-
n += 1
-
end
-
s << "</tr>\n"
-
s.html_safe
-
end
-
-
1
def issues_destroy_confirmation_message(issues)
-
6
issues = [issues] unless issues.is_a?(Array)
-
6
message = l(:text_issues_destroy_confirmation)
-
12
descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
-
6
if descendant_count > 0
-
6
issues.each do |issue|
-
6
next if issue.root?
-
issues.each do |other_issue|
-
descendant_count -= 1 if issue.is_descendant_of?(other_issue)
-
end
-
end
-
6
if descendant_count > 0
-
6
message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
-
end
-
end
-
6
message
-
end
-
-
1
def sidebar_queries
-
38
unless @sidebar_queries
-
19
@sidebar_queries = Query.visible.all(
-
:order => "#{Query.table_name}.name ASC",
-
# Project specific queries and global queries
-
19
:conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
-
)
-
end
-
38
@sidebar_queries
-
end
-
-
1
def query_links(title, queries)
-
# links to #index on issues/show
-
19
url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
-
-
content_tag('h3', h(title)) +
-
queries.collect {|query|
-
95
css = 'query'
-
95
css << ' selected' if query == @query
-
95
link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
-
19
}.join('<br />').html_safe
-
end
-
-
1
def render_sidebar_queries
-
19
out = ''.html_safe
-
114
queries = sidebar_queries.select {|q| !q.is_public?}
-
19
out << query_links(l(:label_my_queries), queries) if queries.any?
-
114
queries = sidebar_queries.select {|q| q.is_public?}
-
19
out << query_links(l(:label_query_plural), queries) if queries.any?
-
19
out
-
end
-
-
# Returns the textual representation of a journal details
-
# as an array of strings
-
1
def details_to_strings(details, no_html=false, options={})
-
282
options[:only_path] = (options[:only_path] == false ? false : true)
-
282
strings = []
-
282
values_by_field = {}
-
282
details.each do |detail|
-
464
if detail.property == 'cf'
-
field_id = detail.prop_key
-
field = CustomField.find_by_id(field_id)
-
if field && field.multiple?
-
values_by_field[field_id] ||= {:added => [], :deleted => []}
-
if detail.old_value
-
values_by_field[field_id][:deleted] << detail.old_value
-
end
-
if detail.value
-
values_by_field[field_id][:added] << detail.value
-
end
-
next
-
end
-
end
-
464
strings << show_detail(detail, no_html, options)
-
end
-
282
values_by_field.each do |field_id, changes|
-
detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
-
if changes[:added].any?
-
detail.value = changes[:added]
-
strings << show_detail(detail, no_html, options)
-
elsif changes[:deleted].any?
-
detail.old_value = changes[:deleted]
-
strings << show_detail(detail, no_html, options)
-
end
-
end
-
282
strings
-
end
-
-
# Returns the textual representation of a single journal detail
-
1
def show_detail(detail, no_html=false, options={})
-
464
multiple = false
-
464
case detail.property
-
when 'attr'
-
464
field = detail.prop_key.to_s.gsub(/\_id$/, "")
-
464
label = l(("field_" + field).to_sym)
-
464
case detail.prop_key
-
when 'due_date', 'start_date'
-
74
value = format_date(detail.value.to_date) if detail.value
-
74
old_value = format_date(detail.old_value.to_date) if detail.old_value
-
-
when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
-
'priority_id', 'category_id', 'fixed_version_id'
-
186
value = find_name_by_reflection(field, detail.value)
-
186
old_value = find_name_by_reflection(field, detail.old_value)
-
-
when 'estimated_hours'
-
2
value = "%0.02f" % detail.value.to_f unless detail.value.blank?
-
2
old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
-
-
when 'parent_id'
-
2
label = l(:field_parent_issue)
-
2
value = "##{detail.value}" unless detail.value.blank?
-
2
old_value = "##{detail.old_value}" unless detail.old_value.blank?
-
-
when 'is_private'
-
value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
-
old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
-
end
-
when 'cf'
-
custom_field = CustomField.find_by_id(detail.prop_key)
-
if custom_field
-
multiple = custom_field.multiple?
-
label = custom_field.name
-
value = format_value(detail.value, custom_field.field_format) if detail.value
-
old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
-
end
-
when 'attachment'
-
label = l(:label_attachment)
-
end
-
464
call_hook(:helper_issues_show_detail_after_setting,
-
{:detail => detail, :label => label, :value => value, :old_value => old_value })
-
-
464
label ||= detail.prop_key
-
464
value ||= detail.value
-
464
old_value ||= detail.old_value
-
-
464
unless no_html
-
232
label = content_tag('strong', label)
-
232
old_value = content_tag("i", h(old_value)) if detail.old_value
-
232
old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank?
-
232
if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
-
# Link to the attachment if it has not been removed
-
value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
-
if options[:only_path] != false && atta.is_text?
-
value += link_to(
-
image_tag('magnifier.png'),
-
:controller => 'attachments', :action => 'show',
-
:id => atta, :filename => atta.filename
-
)
-
end
-
else
-
232
value = content_tag("i", h(value)) if value
-
end
-
end
-
-
464
if detail.property == 'attr' && detail.prop_key == 'description'
-
s = l(:text_journal_changed_no_detail, :label => label)
-
unless no_html
-
diff_link = link_to 'diff',
-
{:controller => 'journals', :action => 'diff', :id => detail.journal_id,
-
:detail_id => detail.id, :only_path => options[:only_path]},
-
:title => l(:label_view_diff)
-
s << " (#{ diff_link })"
-
end
-
s.html_safe
-
464
elsif detail.value.present?
-
464
case detail.property
-
when 'attr', 'cf'
-
464
if detail.old_value.present?
-
366
l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
-
98
elsif multiple
-
l(:text_journal_added, :label => label, :value => value).html_safe
-
else
-
98
l(:text_journal_set_to, :label => label, :value => value).html_safe
-
end
-
when 'attachment'
-
l(:text_journal_added, :label => label, :value => value).html_safe
-
end
-
else
-
l(:text_journal_deleted, :label => label, :old => old_value).html_safe
-
end
-
end
-
-
# Find the name of an associated record stored in the field attribute
-
1
def find_name_by_reflection(field, id)
-
372
association = Issue.reflect_on_association(field.to_sym)
-
372
if association
-
372
record = association.class_name.constantize.find_by_id(id)
-
372
return record.name if record
-
end
-
end
-
-
# Renders issue children recursively
-
1
def render_api_issue_children(issue, api)
-
return if issue.leaf?
-
api.array :children do
-
issue.children.each do |child|
-
api.issue(:id => child.id) do
-
api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
-
api.subject child.subject
-
render_api_issue_children(child, api)
-
end
-
end
-
end
-
end
-
-
1
def issues_to_csv(issues, project, query, options={})
-
decimal_separator = l(:general_csv_decimal_separator)
-
encoding = l(:general_csv_encoding)
-
columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
-
if options[:description]
-
if description = query.available_columns.detect {|q| q.name == :description}
-
columns << description
-
end
-
end
-
-
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
-
# csv header fields
-
csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
-
-
# csv lines
-
issues.each do |issue|
-
col_values = columns.collect do |column|
-
s = if column.is_a?(QueryCustomFieldColumn)
-
cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
-
show_value(cv)
-
else
-
value = column.value(issue)
-
if value.is_a?(Date)
-
format_date(value)
-
elsif value.is_a?(Time)
-
format_time(value)
-
elsif value.is_a?(Float)
-
("%.2f" % value).gsub('.', decimal_separator)
-
else
-
value
-
end
-
end
-
s.to_s
-
end
-
csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) }
-
end
-
end
-
export
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module JournalsHelper
-
1
def render_notes(issue, journal, options={})
-
content = ''
-
editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project)))
-
links = []
-
if !journal.notes.blank?
-
links << link_to(image_tag('comment.png'),
-
{:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal},
-
:remote => true,
-
:method => 'post',
-
:title => l(:button_quote)) if options[:reply_links]
-
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
-
{ :controller => 'journals', :action => 'edit', :id => journal, :format => 'js' },
-
:title => l(:button_edit)) if editable
-
end
-
content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty?
-
content << textilizable(journal, :notes)
-
css_classes = "wiki"
-
css_classes << " editable" if editable
-
content_tag('div', content.html_safe, :id => "journal-#{journal.id}-notes", :class => css_classes)
-
end
-
-
1
def link_to_in_place_notes_editor(text, field_id, url, options={})
-
onclick = "$.ajax({url: '#{url_for(url)}', type: 'get'}); return false;"
-
link_to text, '#', options.merge(:onclick => onclick)
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module MailHandlerHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module MembersHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module MessagesHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module MyHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module NewsHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module ProjectsHelper
-
1
def link_to_version(version, options = {})
-
58
return '' unless version && version.is_a?(Version)
-
58
link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
-
end
-
-
1
def project_settings_tabs
-
7
tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
-
{:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
-
{:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
-
{:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
-
{:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
-
{:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
-
{:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
-
{:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
-
{:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
-
]
-
70
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
-
end
-
-
1
def parent_project_select_tag(project)
-
selected = project.parent
-
# retrieve the requested parent project
-
parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
-
if parent_id
-
selected = (parent_id.blank? ? nil : Project.find(parent_id))
-
end
-
-
options = ''
-
options << "<option value=''></option>" if project.allowed_parents.include?(nil)
-
options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
-
content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id')
-
end
-
-
# Renders the projects index
-
1
def render_project_hierarchy(projects)
-
render_project_nested_lists(projects) do |project|
-
s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'my-project' : nil}")
-
if project.description.present?
-
s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
-
end
-
s
-
end
-
end
-
-
# Returns a set of options for a select field, grouped by project.
-
1
def version_options_for_select(versions, selected=nil)
-
25
grouped = Hash.new {|h,k| h[k] = []}
-
5
versions.each do |version|
-
33
grouped[version.project.name] << [version.name, version.id]
-
end
-
-
5
if grouped.keys.size > 1
-
5
grouped_options_for_select(grouped, selected && selected.id)
-
else
-
options_for_select((grouped.values.first || []), selected && selected.id)
-
end
-
end
-
-
1
def format_version_sharing(sharing)
-
43
sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
-
43
l("label_version_sharing_#{sharing}")
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module QueriesHelper
-
1
def filters_options_for_select(query)
-
16
options_for_select(filters_options(query))
-
end
-
-
1
def filters_options(query)
-
16
options = [[]]
-
16
sorted_options = query.available_filters.sort do |a, b|
-
2208
ord = 0
-
2208
if !(a[1][:order] == 20 && b[1][:order] == 20)
-
2144
ord = a[1][:order] <=> b[1][:order]
-
else
-
64
cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=>
-
64
CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name))
-
64
if cn != 0
-
16
ord = cn
-
else
-
48
f = (a[1][:field] <=> b[1][:field])
-
48
if f != 0
-
48
ord = f
-
else
-
# assigned_to or author
-
ord = (a[0] <=> b[0])
-
end
-
end
-
end
-
2208
ord
-
end
-
16
options += sorted_options.map do |field, field_options|
-
560
[field_options[:name], field]
-
end
-
end
-
-
1
def available_block_columns_tags(query)
-
16
tags = ''.html_safe
-
16
query.available_block_columns.each do |column|
-
16
tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
-
end
-
16
tags
-
end
-
-
1
def column_header(column)
-
91
column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
-
:default_order => column.default_order) :
-
content_tag('th', h(column.caption))
-
end
-
-
1
def column_content(column, issue)
-
607
value = column.value(issue)
-
607
if value.is_a?(Array)
-
value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
-
else
-
607
column_value(column, issue, value)
-
end
-
end
-
-
1
def column_value(column, issue, value)
-
607
case value.class.name
-
when 'String'
-
98
if column.name == :subject
-
98
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
-
elsif column.name == :description
-
issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
-
else
-
h(value)
-
end
-
when 'Time'
-
98
format_time(value)
-
when 'Date'
-
format_date(value)
-
when 'Fixnum'
-
19
if column.name == :done_ratio
-
progress_bar(value, :width => '80px')
-
else
-
19
value.to_s
-
end
-
when 'Float'
-
sprintf "%.2f", value
-
when 'User'
-
10
link_to_user value
-
when 'Project'
-
link_to_project value
-
when 'Version'
-
link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
-
when 'TrueClass'
-
l(:general_text_Yes)
-
when 'FalseClass'
-
l(:general_text_No)
-
when 'Issue'
-
link_to_issue(value, :subject => false)
-
when 'IssueRelation'
-
other = value.other_issue(issue)
-
content_tag('span',
-
(l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
-
:class => value.css_classes_for(issue))
-
else
-
382
h(value)
-
end
-
end
-
-
# Retrieve query from session or build a new query
-
1
def retrieve_query
-
16
if !params[:query_id].blank?
-
cond = "project_id IS NULL"
-
cond << " OR project_id = #{@project.id}" if @project
-
@query = Query.find(params[:query_id], :conditions => cond)
-
raise ::Unauthorized unless @query.visible?
-
@query.project = @project
-
session[:query] = {:id => @query.id, :project_id => @query.project_id}
-
sort_clear
-
16
elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
-
# Give it a name, required to be valid
-
6
@query = Query.new(:name => "_")
-
6
@query.project = @project
-
6
build_query_from_params
-
6
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
-
else
-
# retrieve from session
-
10
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
-
10
@query ||= Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
-
10
@query.project = @project
-
end
-
end
-
-
1
def retrieve_query_from_session
-
3
if session[:query]
-
if session[:query][:id]
-
@query = Query.find_by_id(session[:query][:id])
-
return unless @query
-
else
-
@query = Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
-
end
-
if session[:query].has_key?(:project_id)
-
@query.project_id = session[:query][:project_id]
-
else
-
@query.project = @project
-
end
-
@query
-
end
-
end
-
-
1
def build_query_from_params
-
6
if params[:fields] || params[:f]
-
@query.filters = {}
-
@query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
-
else
-
6
@query.available_filters.keys.each do |field|
-
210
@query.add_short_filter(field, params[field]) if params[field]
-
end
-
end
-
6
@query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
-
6
@query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module ReportsHelper
-
-
1
def aggregate(data, criteria)
-
a = 0
-
data.each { |row|
-
match = 1
-
criteria.each { |k, v|
-
match = 0 unless (row[k].to_s == v.to_s) || (k == 'closed' && row[k] == (v == 0 ? "f" : "t"))
-
} unless criteria.nil?
-
a = a + row["total"].to_i if match == 1
-
} unless data.nil?
-
a
-
end
-
-
1
def aggregate_link(data, criteria, *args)
-
a = aggregate data, criteria
-
a > 0 ? link_to(h(a), *args) : '-'
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module RepositoriesHelper
-
1
def format_revision(revision)
-
if revision.respond_to? :format_identifier
-
revision.format_identifier
-
else
-
revision.to_s
-
end
-
end
-
-
1
def truncate_at_line_break(text, length = 255)
-
if text
-
text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
-
end
-
end
-
-
1
def render_properties(properties)
-
unless properties.nil? || properties.empty?
-
content = ''
-
properties.keys.sort.each do |property|
-
content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>".html_safe)
-
end
-
content_tag('ul', content.html_safe, :class => 'properties')
-
end
-
end
-
-
1
def render_changeset_changes
-
changes = @changeset.filechanges.find(:all, :limit => 1000, :order => 'path').collect do |change|
-
case change.action
-
when 'A'
-
# Detects moved/copied files
-
if !change.from_path.blank?
-
change.action =
-
@changeset.filechanges.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
-
end
-
change
-
when 'D'
-
@changeset.filechanges.detect {|c| c.from_path == change.path} ? nil : change
-
else
-
change
-
end
-
end.compact
-
-
tree = { }
-
changes.each do |change|
-
p = tree
-
dirs = change.path.to_s.split('/').select {|d| !d.blank?}
-
path = ''
-
dirs.each do |dir|
-
path += '/' + dir
-
p[:s] ||= {}
-
p = p[:s]
-
p[path] ||= {}
-
p = p[path]
-
end
-
p[:c] = change
-
end
-
render_changes_tree(tree[:s])
-
end
-
-
1
def render_changes_tree(tree)
-
return '' if tree.nil?
-
output = ''
-
output << '<ul>'
-
tree.keys.sort.each do |file|
-
style = 'change'
-
text = File.basename(h(file))
-
if s = tree[file][:s]
-
style << ' folder'
-
path_param = to_path_param(@repository.relative_path(file))
-
text = link_to(h(text), :controller => 'repositories',
-
:action => 'show',
-
:id => @project,
-
:repository_id => @repository.identifier_param,
-
:path => path_param,
-
:rev => @changeset.identifier)
-
output << "<li class='#{style}'>#{text}"
-
output << render_changes_tree(s)
-
output << "</li>"
-
elsif c = tree[file][:c]
-
style << " change-#{c.action}"
-
path_param = to_path_param(@repository.relative_path(c.path))
-
text = link_to(h(text), :controller => 'repositories',
-
:action => 'entry',
-
:id => @project,
-
:repository_id => @repository.identifier_param,
-
:path => path_param,
-
:rev => @changeset.identifier) unless c.action == 'D'
-
text << " - #{h(c.revision)}" unless c.revision.blank?
-
text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories',
-
:action => 'diff',
-
:id => @project,
-
:repository_id => @repository.identifier_param,
-
:path => path_param,
-
:rev => @changeset.identifier) + ') '.html_safe if c.action == 'M'
-
text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank?
-
output << "<li class='#{style}'>#{text}</li>"
-
end
-
end
-
output << '</ul>'
-
output.html_safe
-
end
-
-
1
def repository_field_tags(form, repository)
-
method = repository.class.name.demodulize.underscore + "_field_tags"
-
if repository.is_a?(Repository) &&
-
respond_to?(method) && method != 'repository_field_tags'
-
send(method, form, repository)
-
end
-
end
-
-
1
def scm_select_tag(repository)
-
scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
-
Redmine::Scm::Base.all.each do |scm|
-
if Setting.enabled_scm.include?(scm) ||
-
(repository && repository.class.name.demodulize == scm)
-
scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
-
end
-
end
-
select_tag('repository_scm',
-
options_for_select(scm_options, repository.class.name.demodulize),
-
:disabled => (repository && !repository.new_record?),
-
:data => {:remote => true, :method => 'get'})
-
end
-
-
1
def with_leading_slash(path)
-
path.to_s.starts_with?('/') ? path : "/#{path}"
-
end
-
-
1
def without_leading_slash(path)
-
path.gsub(%r{^/+}, '')
-
end
-
-
1
def subversion_field_tags(form, repository)
-
content_tag('p', form.text_field(:url, :size => 60, :required => true,
-
:disabled => !repository.safe_attribute?('url')) +
-
'<br />'.html_safe +
-
'(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
-
content_tag('p', form.text_field(:login, :size => 30)) +
-
content_tag('p', form.password_field(
-
:password, :size => 30, :name => 'ignore',
-
:value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
-
:onfocus => "this.value=''; this.name='repository[password]';",
-
:onchange => "this.name='repository[password]';"))
-
end
-
-
1
def darcs_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:url, :label => l(:field_path_to_repository),
-
:size => 60, :required => true,
-
:disabled => !repository.safe_attribute?('url'))) +
-
content_tag('p', form.select(
-
:log_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_commit_logs_encoding), :required => true))
-
end
-
-
1
def mercurial_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:url, :label => l(:field_path_to_repository),
-
:size => 60, :required => true,
-
:disabled => !repository.safe_attribute?('url')
-
) +
-
'<br />'.html_safe + l(:text_mercurial_repository_note)) +
-
content_tag('p', form.select(
-
:path_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_scm_path_encoding)
-
) +
-
'<br />'.html_safe + l(:text_scm_path_encoding_note))
-
end
-
-
1
def git_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:url, :label => l(:field_path_to_repository),
-
:size => 60, :required => true,
-
:disabled => !repository.safe_attribute?('url')
-
) +
-
'<br />'.html_safe +
-
l(:text_git_repository_note)) +
-
content_tag('p', form.select(
-
:path_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_scm_path_encoding)
-
) +
-
'<br />'.html_safe + l(:text_scm_path_encoding_note)) +
-
content_tag('p', form.check_box(
-
:extra_report_last_commit,
-
:label => l(:label_git_report_last_commit)
-
))
-
end
-
-
1
def cvs_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:root_url,
-
:label => l(:field_cvsroot),
-
:size => 60, :required => true,
-
:disabled => !repository.safe_attribute?('root_url'))) +
-
content_tag('p', form.text_field(
-
:url,
-
:label => l(:field_cvs_module),
-
:size => 30, :required => true,
-
:disabled => !repository.safe_attribute?('url'))) +
-
content_tag('p', form.select(
-
:log_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_commit_logs_encoding), :required => true)) +
-
content_tag('p', form.select(
-
:path_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_scm_path_encoding)
-
) +
-
'<br />'.html_safe + l(:text_scm_path_encoding_note))
-
end
-
-
1
def bazaar_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:url, :label => l(:field_path_to_repository),
-
:size => 60, :required => true,
-
:disabled => !repository.safe_attribute?('url'))) +
-
content_tag('p', form.select(
-
:log_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_commit_logs_encoding), :required => true))
-
end
-
-
1
def filesystem_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:url, :label => l(:field_root_directory),
-
:size => 60, :required => true,
-
:disabled => !repository.safe_attribute?('url'))) +
-
content_tag('p', form.select(
-
:path_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_scm_path_encoding)
-
) +
-
'<br />'.html_safe + l(:text_scm_path_encoding_note))
-
end
-
-
1
def index_commits(commits, heads)
-
return nil if commits.nil? or commits.first.parents.nil?
-
refs_map = {}
-
heads.each do |head|
-
refs_map[head.scmid] ||= []
-
refs_map[head.scmid] << head
-
end
-
commits_by_scmid = {}
-
commits.reverse.each_with_index do |commit, commit_index|
-
commits_by_scmid[commit.scmid] = {
-
:parent_scmids => commit.parents.collect { |parent| parent.scmid },
-
:rdmid => commit_index,
-
:refs => refs_map.include?(commit.scmid) ? refs_map[commit.scmid].join(" ") : nil,
-
:scmid => commit.scmid,
-
:href => block_given? ? yield(commit.scmid) : commit.scmid
-
}
-
end
-
heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
-
space = nil
-
heads.each do |head|
-
if commits_by_scmid.include? head.scmid
-
space = index_head((space || -1) + 1, head, commits_by_scmid)
-
end
-
end
-
# when no head matched anything use first commit
-
space ||= index_head(0, commits.first, commits_by_scmid)
-
return commits_by_scmid, space
-
end
-
-
1
def index_head(space, commit, commits_by_scmid)
-
stack = [[space, commits_by_scmid[commit.scmid]]]
-
max_space = space
-
until stack.empty?
-
space, commit = stack.pop
-
commit[:space] = space if commit[:space].nil?
-
space -= 1
-
commit[:parent_scmids].each_with_index do |parent_scmid, parent_index|
-
parent_commit = commits_by_scmid[parent_scmid]
-
if parent_commit and parent_commit[:space].nil?
-
stack.unshift [space += 1, parent_commit]
-
end
-
end
-
max_space = space if max_space < space
-
end
-
max_space
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module RolesHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module SearchHelper
-
1
def highlight_tokens(text, tokens)
-
return text unless text && tokens && !tokens.empty?
-
re_tokens = tokens.collect {|t| Regexp.escape(t)}
-
regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
-
result = ''
-
text.split(regexp).each_with_index do |words, i|
-
if result.length > 1200
-
# maximum length of the preview reached
-
result << '...'
-
break
-
end
-
words = words.mb_chars
-
if i.even?
-
result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words)
-
else
-
t = (tokens.index(words.downcase) || 0) % 4
-
result << content_tag('span', h(words), :class => "highlight token-#{t}")
-
end
-
end
-
result.html_safe
-
end
-
-
1
def type_label(t)
-
l("label_#{t.singularize}_plural", :default => t.to_s.humanize)
-
end
-
-
1
def project_select_tag
-
options = [[l(:label_project_all), 'all']]
-
options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
-
options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty?
-
options << [@project.name, ''] unless @project.nil?
-
label_tag("scope", l(:description_project_scope), :class => "hidden-for-sighted") +
-
select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
-
end
-
-
1
def render_results_by_type(results_by_type)
-
links = []
-
# Sorts types by results count
-
results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
-
c = results_by_type[t]
-
next if c == 0
-
text = "#{type_label(t)} (#{c})"
-
links << link_to(h(text), :q => params[:q], :titles_only => params[:titles_only],
-
:all_words => params[:all_words], :scope => params[:scope], t => 1)
-
end
-
('<ul>'.html_safe +
-
links.map {|link| content_tag('li', link)}.join(' ').html_safe +
-
'</ul>'.html_safe) unless links.empty?
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module SettingsHelper
-
1
def administration_settings_tabs
-
tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
-
{:name => 'display', :partial => 'settings/display', :label => :label_display},
-
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
-
{:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
-
{:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
-
{:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
-
{:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
-
{:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
-
]
-
end
-
-
1
def setting_select(setting, choices, options={})
-
if blank_text = options.delete(:blank)
-
choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
-
end
-
setting_label(setting, options).html_safe +
-
select_tag("settings[#{setting}]",
-
options_for_select(choices, Setting.send(setting).to_s),
-
options).html_safe
-
end
-
-
1
def setting_multiselect(setting, choices, options={})
-
setting_values = Setting.send(setting)
-
setting_values = [] unless setting_values.is_a?(Array)
-
-
content_tag("label", l(options[:label] || "setting_#{setting}")) +
-
hidden_field_tag("settings[#{setting}][]", '').html_safe +
-
choices.collect do |choice|
-
text, value = (choice.is_a?(Array) ? choice : [choice, choice])
-
content_tag(
-
'label',
-
check_box_tag(
-
"settings[#{setting}][]",
-
value,
-
Setting.send(setting).include?(value),
-
:id => nil
-
) + text.to_s,
-
:class => (options[:inline] ? 'inline' : 'block')
-
)
-
end.join.html_safe
-
end
-
-
1
def setting_text_field(setting, options={})
-
setting_label(setting, options).html_safe +
-
text_field_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
-
end
-
-
1
def setting_text_area(setting, options={})
-
setting_label(setting, options).html_safe +
-
text_area_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
-
end
-
-
1
def setting_check_box(setting, options={})
-
setting_label(setting, options).html_safe +
-
hidden_field_tag("settings[#{setting}]", 0, :id => nil).html_safe +
-
check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options).html_safe
-
end
-
-
1
def setting_label(setting, options={})
-
label = options.delete(:label)
-
label != false ? label_tag("settings_#{setting}", l(label || "setting_#{setting}")).html_safe : ''
-
end
-
-
# Renders a notification field for a Redmine::Notifiable option
-
1
def notification_field(notifiable)
-
return content_tag(:label,
-
check_box_tag('settings[notified_events][]',
-
notifiable.name,
-
Setting.notified_events.include?(notifiable.name), :id => nil).html_safe +
-
l_or_humanize(notifiable.name, :prefix => 'label_').html_safe,
-
:class => notifiable.parent.present? ? "parent" : '').html_safe
-
end
-
-
1
def cross_project_subtasks_options
-
options = [
-
[:label_disabled, ''],
-
[:label_cross_project_system, 'system'],
-
[:label_cross_project_tree, 'tree'],
-
[:label_cross_project_hierarchy, 'hierarchy'],
-
[:label_cross_project_descendants, 'descendants']
-
]
-
-
options.map {|label, value| [l(label), value.to_s]}
-
end
-
end
-
# encoding: utf-8
-
#
-
# Helpers to sort tables using clickable column headers.
-
#
-
# Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
-
# Jean-Philippe Lang, 2009
-
# License: This source code is released under the MIT license.
-
#
-
# - Consecutive clicks toggle the column's sort order.
-
# - Sort state is maintained by a session hash entry.
-
# - CSS classes identify sort column and state.
-
# - Typically used in conjunction with the Pagination module.
-
#
-
# Example code snippets:
-
#
-
# Controller:
-
#
-
# helper :sort
-
# include SortHelper
-
#
-
# def list
-
# sort_init 'last_name'
-
# sort_update %w(first_name last_name)
-
# @items = Contact.find_all nil, sort_clause
-
# end
-
#
-
# Controller (using Pagination module):
-
#
-
# helper :sort
-
# include SortHelper
-
#
-
# def list
-
# sort_init 'last_name'
-
# sort_update %w(first_name last_name)
-
# @contact_pages, @items = paginate :contacts,
-
# :order_by => sort_clause,
-
# :per_page => 10
-
# end
-
#
-
# View (table header in list.rhtml):
-
#
-
# <thead>
-
# <tr>
-
# <%= sort_header_tag('id', :title => 'Sort by contact ID') %>
-
# <%= sort_header_tag('last_name', :caption => 'Name') %>
-
# <%= sort_header_tag('phone') %>
-
# <%= sort_header_tag('address', :width => 200) %>
-
# </tr>
-
# </thead>
-
#
-
# - Introduces instance variables: @sort_default, @sort_criteria
-
# - Introduces param :sort
-
#
-
-
1
module SortHelper
-
1
class SortCriteria
-
-
1
def initialize
-
143
@criteria = []
-
end
-
-
1
def available_criteria=(criteria)
-
20
unless criteria.is_a?(Hash)
-
criteria = criteria.inject({}) {|h,k| h[k] = k; h}
-
end
-
20
@available_criteria = criteria
-
end
-
-
1
def from_param(param)
-
277
@criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]}
-
143
normalize!
-
end
-
-
1
def criteria=(arg)
-
9
@criteria = arg
-
9
normalize!
-
end
-
-
1
def to_param
-
638
@criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
-
end
-
-
1
def to_sql
-
20
sql = @criteria.collect do |k,o|
-
20
if s = @available_criteria[k]
-
35
(o ? s.to_a : s.to_a.collect {|c| append_desc(c)}).join(', ')
-
end
-
end.compact.join(', ')
-
20
sql.blank? ? nil : sql
-
end
-
-
1
def to_a
-
16
@criteria.dup
-
end
-
-
1
def add!(key, asc)
-
246
@criteria.delete_if {|k,o| k == key}
-
123
@criteria = [[key, asc]] + @criteria
-
123
normalize!
-
end
-
-
1
def add(*args)
-
123
r = self.class.new.from_param(to_param)
-
123
r.add!(*args)
-
123
r
-
end
-
-
1
def first_key
-
123
@criteria.first && @criteria.first.first
-
end
-
-
1
def first_asc?
-
17
@criteria.first && @criteria.first.last
-
end
-
-
1
def empty?
-
20
@criteria.empty?
-
end
-
-
1
private
-
-
1
def normalize!
-
275
@criteria ||= []
-
647
@criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]}
-
295
@criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
-
275
@criteria.slice!(3)
-
275
self
-
end
-
-
# Appends DESC to the sort criterion unless it has a fixed order
-
1
def append_desc(criterion)
-
15
if criterion =~ / (asc|desc)$/i
-
criterion
-
else
-
15
"#{criterion} DESC"
-
end
-
end
-
end
-
-
1
def sort_name
-
20
controller_name + '_' + action_name + '_sort'
-
end
-
-
# Initializes the default sort.
-
# Examples:
-
#
-
# sort_init 'name'
-
# sort_init 'id', 'desc'
-
# sort_init ['name', ['id', 'desc']]
-
# sort_init [['name', 'desc'], ['id', 'desc']]
-
#
-
1
def sort_init(*args)
-
20
case args.size
-
when 1
-
16
@sort_default = args.first.is_a?(Array) ? args.first : [[args.first]]
-
when 2
-
4
@sort_default = [[args.first, args.last]]
-
else
-
raise ArgumentError
-
end
-
end
-
-
# Updates the sort state. Call this in the controller prior to calling
-
# sort_clause.
-
# - criteria can be either an array or a hash of allowed keys
-
#
-
1
def sort_update(criteria, sort_name=nil)
-
20
sort_name ||= self.sort_name
-
20
@sort_criteria = SortCriteria.new
-
20
@sort_criteria.available_criteria = criteria
-
20
@sort_criteria.from_param(params[:sort] || session[sort_name])
-
20
@sort_criteria.criteria = @sort_default if @sort_criteria.empty?
-
20
session[sort_name] = @sort_criteria.to_param
-
end
-
-
# Clears the sort criteria session data
-
#
-
1
def sort_clear
-
session[sort_name] = nil
-
end
-
-
# Returns an SQL sort clause corresponding to the current sort state.
-
# Use this to sort the controller's table items collection.
-
#
-
1
def sort_clause()
-
20
@sort_criteria.to_sql
-
end
-
-
1
def sort_criteria
-
16
@sort_criteria
-
end
-
-
# Returns a link which sorts by the named column.
-
#
-
# - column is the name of an attribute in the sorted record collection.
-
# - the optional caption explicitly specifies the displayed link text.
-
# - 2 CSS classes reflect the state of the link: sort and asc or desc
-
#
-
1
def sort_link(column, caption, default_order)
-
123
css, order = nil, default_order
-
-
123
if column.to_s == @sort_criteria.first_key
-
17
if @sort_criteria.first_asc?
-
7
css = 'sort asc'
-
7
order = 'desc'
-
else
-
10
css = 'sort desc'
-
10
order = 'asc'
-
end
-
end
-
123
caption = column.to_s.humanize unless caption
-
-
123
sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
-
123
url_options = params.merge(sort_options)
-
-
# Add project_id to url_options
-
123
url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
-
-
123
link_to_content_update(h(caption), url_options, :class => css)
-
end
-
-
# Returns a table header <th> tag with a sort link for the named column
-
# attribute.
-
#
-
# Options:
-
# :caption The displayed link name (defaults to titleized column name).
-
# :title The tag's 'title' attribute (defaults to 'Sort by :caption').
-
#
-
# Other options hash entries generate additional table header tag attributes.
-
#
-
# Example:
-
#
-
# <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
-
#
-
1
def sort_header_tag(column, options = {})
-
123
caption = options.delete(:caption) || column.to_s.humanize
-
123
default_order = options.delete(:default_order) || 'asc'
-
123
options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title]
-
123
content_tag('th', sort_link(column, caption, default_order), options)
-
end
-
end
-
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module TimelogHelper
-
1
include ApplicationHelper
-
-
1
def render_timelog_breadcrumb
-
4
links = []
-
4
links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
-
4
links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
-
4
if @issue
-
2
if @issue.visible?
-
2
links << link_to_issue(@issue, :subject => false)
-
else
-
links << "##{@issue.id}"
-
end
-
end
-
4
breadcrumb links
-
end
-
-
# Returns a collection of activities for a select field. time_entry
-
# is optional and will be used to check if the selected TimeEntryActivity
-
# is active.
-
1
def activity_collection_for_select_options(time_entry=nil, project=nil)
-
5
project ||= @project
-
5
if project.nil?
-
activities = TimeEntryActivity.shared.active
-
else
-
5
activities = project.activities
-
end
-
-
5
collection = []
-
5
if time_entry && time_entry.activity && !time_entry.activity.active?
-
collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
-
else
-
5
collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
-
end
-
20
activities.each { |a| collection << [a.name, a.id] }
-
5
collection
-
end
-
-
1
def select_hours(data, criteria, value)
-
if value.to_s.empty?
-
data.select {|row| row[criteria].blank? }
-
else
-
data.select {|row| row[criteria].to_s == value.to_s}
-
end
-
end
-
-
1
def sum_hours(data)
-
sum = 0
-
data.each do |row|
-
sum += row['hours'].to_f
-
end
-
sum
-
end
-
-
1
def options_for_period_select(value)
-
options_for_select([[l(:label_all_time), 'all'],
-
[l(:label_today), 'today'],
-
[l(:label_yesterday), 'yesterday'],
-
[l(:label_this_week), 'current_week'],
-
[l(:label_last_week), 'last_week'],
-
[l(:label_last_n_weeks, 2), 'last_2_weeks'],
-
[l(:label_last_n_days, 7), '7_days'],
-
[l(:label_this_month), 'current_month'],
-
[l(:label_last_month), 'last_month'],
-
[l(:label_last_n_days, 30), '30_days'],
-
4
[l(:label_this_year), 'current_year']],
-
value)
-
end
-
-
1
def entries_to_csv(entries)
-
decimal_separator = l(:general_csv_decimal_separator)
-
custom_fields = TimeEntryCustomField.find(:all)
-
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
-
# csv header fields
-
headers = [l(:field_spent_on),
-
l(:field_user),
-
l(:field_activity),
-
l(:field_project),
-
l(:field_issue),
-
l(:field_tracker),
-
l(:field_subject),
-
l(:field_hours),
-
l(:field_comments)
-
]
-
# Export custom fields
-
headers += custom_fields.collect(&:name)
-
-
csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
-
c.to_s,
-
l(:general_csv_encoding) ) }
-
# csv lines
-
entries.each do |entry|
-
fields = [format_date(entry.spent_on),
-
entry.user,
-
entry.activity,
-
entry.project,
-
(entry.issue ? entry.issue.id : nil),
-
(entry.issue ? entry.issue.tracker : nil),
-
(entry.issue ? entry.issue.subject : nil),
-
entry.hours.to_s.gsub('.', decimal_separator),
-
entry.comments
-
]
-
fields += custom_fields.collect {|f| show_value(entry.custom_field_values.detect {|v| v.custom_field_id == f.id}) }
-
-
csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(
-
c.to_s,
-
l(:general_csv_encoding) ) }
-
end
-
end
-
export
-
end
-
-
1
def format_criteria_value(criteria_options, value)
-
if value.blank?
-
"[#{l(:label_none)}]"
-
elsif k = criteria_options[:klass]
-
obj = k.find_by_id(value.to_i)
-
if obj.is_a?(Issue)
-
obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
-
else
-
obj
-
end
-
else
-
format_value(value, criteria_options[:format])
-
end
-
end
-
-
1
def report_to_csv(report)
-
decimal_separator = l(:general_csv_decimal_separator)
-
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
-
# Column headers
-
headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
-
headers += report.periods
-
headers << l(:label_total)
-
csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
-
c.to_s,
-
l(:general_csv_encoding) ) }
-
# Content
-
report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
-
# Total row
-
str_total = Redmine::CodesetUtil.from_utf8(l(:label_total), l(:general_csv_encoding))
-
row = [ str_total ] + [''] * (report.criteria.size - 1)
-
total = 0
-
report.periods.each do |period|
-
sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
-
total += sum
-
row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
-
end
-
row << ("%.2f" % total).gsub('.',decimal_separator)
-
csv << row
-
end
-
export
-
end
-
-
1
def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
-
decimal_separator = l(:general_csv_decimal_separator)
-
hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
-
hours_for_value = select_hours(hours, criteria[level], value)
-
next if hours_for_value.empty?
-
row = [''] * level
-
row << Redmine::CodesetUtil.from_utf8(
-
format_criteria_value(available_criteria[criteria[level]], value).to_s,
-
l(:general_csv_encoding) )
-
row += [''] * (criteria.length - level - 1)
-
total = 0
-
periods.each do |period|
-
sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
-
total += sum
-
row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
-
end
-
row << ("%.2f" % total).gsub('.',decimal_separator)
-
csv << row
-
if criteria.length > level + 1
-
report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module TrackersHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module UsersHelper
-
1
def users_status_options_for_select(selected)
-
user_count_by_status = User.count(:group => 'status').to_hash
-
options_for_select([[l(:label_all), ''],
-
["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'],
-
["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'],
-
["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s)
-
end
-
-
1
def user_mail_notification_options(user)
-
user.valid_notification_options.collect {|o| [l(o.last), o.first]}
-
end
-
-
1
def change_status_link(user)
-
url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
-
-
if user.locked?
-
link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
-
elsif user.registered?
-
link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
-
elsif user != User.current
-
link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock'
-
end
-
end
-
-
1
def user_settings_tabs
-
tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
-
{:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
-
]
-
if Group.all.any?
-
tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}
-
end
-
tabs
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module VersionsHelper
-
-
1
def version_anchor(version)
-
if @project == version.project
-
anchor version.name
-
else
-
anchor "#{version.project.try(:identifier)}-#{version.name}"
-
end
-
end
-
-
1
STATUS_BY_CRITERIAS = %w(tracker status priority author assigned_to category)
-
-
1
def render_issue_status_by(version, criteria)
-
1
criteria = 'tracker' unless STATUS_BY_CRITERIAS.include?(criteria)
-
-
3
h = Hash.new {|k,v| k[v] = [0, 0]}
-
1
begin
-
# Total issue count
-
1
Issue.count(:group => criteria,
-
2
:conditions => ["#{Issue.table_name}.fixed_version_id = ?", version.id]).each {|c,s| h[c][0] = s}
-
# Open issues count
-
1
Issue.count(:group => criteria,
-
:include => :status,
-
2
:conditions => ["#{Issue.table_name}.fixed_version_id = ? AND #{IssueStatus.table_name}.is_closed = ?", version.id, false]).each {|c,s| h[c][1] = s}
-
rescue ActiveRecord::RecordNotFound
-
# When grouping by an association, Rails throws this exception if there's no result (bug)
-
end
-
# Sort with nil keys in last position
-
4
counts = h.keys.sort {|a,b| a.nil? ? 1 : (b.nil? ? -1 : a <=> b)}.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}}
-
3
max = counts.collect {|c| c[:total]}.max
-
-
1
render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
-
end
-
-
1
def status_by_options_for_select(value)
-
7
options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module WatchersHelper
-
-
1
def watcher_tag(object, user, options={})
-
8
content_tag("span", watcher_link(object, user), :class => watcher_css(object))
-
end
-
-
1
def watcher_link(object, user)
-
8
return '' unless user && user.logged? && object.respond_to?('watched_by?')
-
8
watched = object.watched_by?(user)
-
8
url = {:controller => 'watchers',
-
8
:action => (watched ? 'unwatch' : 'watch'),
-
:object_type => object.class.to_s.underscore,
-
:object_id => object.id}
-
8
link_to((watched ? l(:button_unwatch) : l(:button_watch)), url,
-
8
:remote => true, :method => 'post', :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
-
-
end
-
-
# Returns the css class used to identify watch links for a given +object+
-
1
def watcher_css(object)
-
8
"#{object.class.to_s.underscore}-#{object.id}-watcher"
-
end
-
-
# Returns a comma separated list of users watching the given object
-
1
def watchers_list(object)
-
3
remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
-
3
content = ''.html_safe
-
3
lis = object.watcher_users.collect do |user|
-
s = ''.html_safe
-
s << avatar(user, :size => "16").to_s
-
s << link_to_user(user, :class => 'user')
-
if remove_allowed
-
url = {:controller => 'watchers',
-
:action => 'destroy',
-
:object_type => object.class.to_s.underscore,
-
:object_id => object.id,
-
:user_id => user}
-
s << ' '
-
s << link_to(image_tag('delete.png'), url,
-
:remote => true, :method => 'post', :style => "vertical-align: middle", :class => "delete")
-
end
-
content << content_tag('li', s)
-
end
-
3
content.present? ? content_tag('ul', content) : content
-
end
-
-
1
def watchers_checkboxes(object, users, checked=nil)
-
2
users.map do |user|
-
4
c = checked.nil? ? object.watched_by?(user) : checked
-
4
tag = check_box_tag 'issue[watcher_user_ids][]', user.id, c, :id => nil
-
4
content_tag 'label', "#{tag} #{h(user)}".html_safe,
-
:id => "issue_watcher_user_ids_#{user.id}",
-
:class => "floating"
-
end.join.html_safe
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module WelcomeHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module WikiHelper
-
-
1
def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
-
20
pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
-
20
s = ''.html_safe
-
20
if pages.has_key?(parent)
-
8
pages[parent].each do |page|
-
18
attrs = "value='#{page.id}'"
-
18
attrs << " selected='selected'" if selected == page
-
18
indent = (level > 0) ? (' ' * level * 2 + '» ') : ''
-
-
18
s << content_tag('option', (indent + h(page.pretty_title)).html_safe, :value => page.id.to_s, :selected => selected == page) +
-
18
wiki_page_options_for_select(pages, selected, page, level + 1)
-
end
-
end
-
20
s
-
end
-
-
1
def wiki_page_breadcrumb(page)
-
breadcrumb(page.ancestors.reverse.collect {|parent|
-
link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project, :version => nil})
-
4
})
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module WorkflowsHelper
-
1
def field_required?(field)
-
field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
-
end
-
-
1
def field_permission_tag(permissions, status, field)
-
name = field.is_a?(CustomField) ? field.id.to_s : field
-
options = [["", ""], [l(:label_readonly), "readonly"]]
-
options << [l(:label_required), "required"] unless field_required?(field)
-
-
select_tag("permissions[#{name}][#{status.id}]", options_for_select(options, permissions[status.id][name]))
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require "digest/md5"
-
-
1
class Attachment < ActiveRecord::Base
-
1
belongs_to :container, :polymorphic => true
-
1
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
-
-
1
validates_presence_of :filename, :author
-
1
validates_length_of :filename, :maximum => 255
-
1
validates_length_of :disk_filename, :maximum => 255
-
1
validates_length_of :description, :maximum => 255
-
1
validate :validate_max_file_size
-
-
acts_as_event :title => :filename,
-
1
:url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
-
-
acts_as_activity_provider :type => 'files',
-
:permission => :view_files,
-
:author_key => :author_id,
-
:find_options => {:select => "#{Attachment.table_name}.*",
-
:joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
-
1
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
-
-
acts_as_activity_provider :type => 'documents',
-
:permission => :view_documents,
-
:author_key => :author_id,
-
:find_options => {:select => "#{Attachment.table_name}.*",
-
:joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
-
1
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
-
-
1
cattr_accessor :storage_path
-
1
@@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
-
-
1
cattr_accessor :thumbnails_storage_path
-
1
@@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
-
-
1
before_save :files_to_final_location
-
1
after_destroy :delete_from_disk
-
-
# Returns an unsaved copy of the attachment
-
1
def copy(attributes=nil)
-
copy = self.class.new
-
copy.attributes = self.attributes.dup.except("id", "downloads")
-
copy.attributes = attributes if attributes
-
copy
-
end
-
-
1
def validate_max_file_size
-
if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
-
errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
-
end
-
end
-
-
1
def file=(incoming_file)
-
unless incoming_file.nil?
-
@temp_file = incoming_file
-
if @temp_file.size > 0
-
if @temp_file.respond_to?(:original_filename)
-
self.filename = @temp_file.original_filename
-
self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
-
end
-
if @temp_file.respond_to?(:content_type)
-
self.content_type = @temp_file.content_type.to_s.chomp
-
end
-
if content_type.blank? && filename.present?
-
self.content_type = Redmine::MimeType.of(filename)
-
end
-
self.filesize = @temp_file.size
-
end
-
end
-
end
-
-
1
def file
-
nil
-
end
-
-
1
def filename=(arg)
-
write_attribute :filename, sanitize_filename(arg.to_s)
-
if new_record? && disk_filename.blank?
-
self.disk_filename = Attachment.disk_filename(filename)
-
end
-
filename
-
end
-
-
# Copies the temporary file to its final location
-
# and computes its MD5 hash
-
1
def files_to_final_location
-
if @temp_file && (@temp_file.size > 0)
-
logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
-
md5 = Digest::MD5.new
-
File.open(diskfile, "wb") do |f|
-
if @temp_file.respond_to?(:read)
-
buffer = ""
-
while (buffer = @temp_file.read(8192))
-
f.write(buffer)
-
md5.update(buffer)
-
end
-
else
-
f.write(@temp_file)
-
md5.update(@temp_file)
-
end
-
end
-
self.digest = md5.hexdigest
-
end
-
@temp_file = nil
-
# Don't save the content type if it's longer than the authorized length
-
if self.content_type && self.content_type.length > 255
-
self.content_type = nil
-
end
-
end
-
-
# Deletes the file from the file system if it's not referenced by other attachments
-
1
def delete_from_disk
-
1215
if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
-
671
delete_from_disk!
-
end
-
end
-
-
# Returns file's location on disk
-
1
def diskfile
-
671
File.join(self.class.storage_path, disk_filename.to_s)
-
end
-
-
1
def title
-
title = filename.to_s
-
if description.present?
-
title << " (#{description})"
-
end
-
title
-
end
-
-
1
def increment_download
-
increment!(:downloads)
-
end
-
-
1
def project
-
container.try(:project)
-
end
-
-
1
def visible?(user=User.current)
-
container && container.attachments_visible?(user)
-
end
-
-
1
def deletable?(user=User.current)
-
container && container.attachments_deletable?(user)
-
end
-
-
1
def image?
-
!!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
-
end
-
-
1
def thumbnailable?
-
image?
-
end
-
-
# Returns the full path the attachment thumbnail, or nil
-
# if the thumbnail cannot be generated.
-
1
def thumbnail(options={})
-
if thumbnailable? && readable?
-
size = options[:size].to_i
-
if size > 0
-
# Limit the number of thumbnails per image
-
size = (size / 50) * 50
-
# Maximum thumbnail size
-
size = 800 if size > 800
-
else
-
size = Setting.thumbnails_size.to_i
-
end
-
size = 100 unless size > 0
-
target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
-
-
begin
-
Redmine::Thumbnail.generate(self.diskfile, target, size)
-
rescue => e
-
logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
-
return nil
-
end
-
end
-
end
-
-
# Deletes all thumbnails
-
1
def self.clear_thumbnails
-
Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
-
File.delete file
-
end
-
end
-
-
1
def is_text?
-
Redmine::MimeType.is_type?('text', filename)
-
end
-
-
1
def is_diff?
-
self.filename =~ /\.(patch|diff)$/i
-
end
-
-
# Returns true if the file is readable
-
1
def readable?
-
File.readable?(diskfile)
-
end
-
-
# Returns the attachment token
-
1
def token
-
"#{id}.#{digest}"
-
end
-
-
# Finds an attachment that matches the given token and that has no container
-
1
def self.find_by_token(token)
-
if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
-
attachment_id, attachment_digest = $1, $2
-
attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
-
if attachment && attachment.container.nil?
-
attachment
-
end
-
end
-
end
-
-
# Bulk attaches a set of files to an object
-
#
-
# Returns a Hash of the results:
-
# :files => array of the attached files
-
# :unsaved => array of the files that could not be attached
-
1
def self.attach_files(obj, attachments)
-
result = obj.save_attachments(attachments, User.current)
-
obj.attach_saved_attachments
-
result
-
end
-
-
1
def self.latest_attach(attachments, filename)
-
attachments.sort_by(&:created_on).reverse.detect {
-
|att| att.filename.downcase == filename.downcase
-
}
-
end
-
-
1
def self.prune(age=1.day)
-
Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
-
end
-
-
1
private
-
-
# Physically deletes the file from the file system
-
1
def delete_from_disk!
-
671
if disk_filename.present? && File.exist?(diskfile)
-
File.delete(diskfile)
-
end
-
end
-
-
1
def sanitize_filename(value)
-
# get only the filename, not the whole path
-
just_filename = value.gsub(/^.*(\\|\/)/, '')
-
-
# Finally, replace invalid characters with underscore
-
@filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
-
end
-
-
# Returns an ASCII or hashed filename
-
1
def self.disk_filename(filename)
-
timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
-
ascii = ''
-
if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
-
ascii = filename
-
else
-
ascii = Digest::MD5.hexdigest(filename)
-
# keep the extension if any
-
ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
-
end
-
while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
-
timestamp.succ!
-
end
-
"#{timestamp}_#{ascii}"
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
# Generic exception for when the AuthSource can not be reached
-
# (eg. can not connect to the LDAP)
-
1
class AuthSourceException < Exception; end
-
1
class AuthSourceTimeoutException < AuthSourceException; end
-
-
1
class AuthSource < ActiveRecord::Base
-
1
include Redmine::SubclassFactory
-
1
include Redmine::Ciphering
-
-
1
has_many :users
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name
-
1
validates_length_of :name, :maximum => 60
-
-
1
def authenticate(login, password)
-
end
-
-
1
def test_connection
-
end
-
-
1
def auth_method_name
-
"Abstract"
-
end
-
-
1
def account_password
-
read_ciphered_attribute(:account_password)
-
end
-
-
1
def account_password=(arg)
-
write_ciphered_attribute(:account_password, arg)
-
end
-
-
1
def allow_password_changes?
-
self.class.allow_password_changes?
-
end
-
-
# Does this auth source backend allow password changes?
-
1
def self.allow_password_changes?
-
false
-
end
-
-
# Try to authenticate a user not yet registered against available sources
-
1
def self.authenticate(login, password)
-
AuthSource.where(:onthefly_register => true).all.each do |source|
-
begin
-
logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
-
attrs = source.authenticate(login, password)
-
rescue => e
-
logger.error "Error during authentication: #{e.message}"
-
attrs = nil
-
end
-
return attrs if attrs
-
end
-
return nil
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'iconv'
-
1
require 'net/ldap'
-
1
require 'net/ldap/dn'
-
1
require 'timeout'
-
-
1
class AuthSourceLdap < AuthSource
-
1
validates_presence_of :host, :port, :attr_login
-
1
validates_length_of :name, :host, :maximum => 60, :allow_nil => true
-
1
validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true
-
1
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
-
1
validates_numericality_of :port, :only_integer => true
-
1
validates_numericality_of :timeout, :only_integer => true, :allow_blank => true
-
1
validate :validate_filter
-
-
1
before_validation :strip_ldap_attributes
-
-
1
def initialize(attributes=nil, *args)
-
super
-
self.port = 389 if self.port == 0
-
end
-
-
1
def authenticate(login, password)
-
return nil if login.blank? || password.blank?
-
-
with_timeout do
-
attrs = get_user_dn(login, password)
-
if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
-
logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
-
return attrs.except(:dn)
-
end
-
end
-
rescue Net::LDAP::LdapError => e
-
raise AuthSourceException.new(e.message)
-
end
-
-
# test the connection to the LDAP
-
1
def test_connection
-
with_timeout do
-
ldap_con = initialize_ldap_con(self.account, self.account_password)
-
ldap_con.open { }
-
end
-
rescue Net::LDAP::LdapError => e
-
raise AuthSourceException.new(e.message)
-
end
-
-
1
def auth_method_name
-
"LDAP"
-
end
-
-
1
private
-
-
1
def with_timeout(&block)
-
timeout = self.timeout
-
timeout = 20 unless timeout && timeout > 0
-
Timeout.timeout(timeout) do
-
return yield
-
end
-
rescue Timeout::Error => e
-
raise AuthSourceTimeoutException.new(e.message)
-
end
-
-
1
def ldap_filter
-
if filter.present?
-
Net::LDAP::Filter.construct(filter)
-
end
-
rescue Net::LDAP::LdapError
-
nil
-
end
-
-
1
def validate_filter
-
if filter.present? && ldap_filter.nil?
-
errors.add(:filter, :invalid)
-
end
-
end
-
-
1
def strip_ldap_attributes
-
[:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
-
write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
-
end
-
end
-
-
1
def initialize_ldap_con(ldap_user, ldap_password)
-
options = { :host => self.host,
-
:port => self.port,
-
:encryption => (self.tls ? :simple_tls : nil)
-
}
-
options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
-
Net::LDAP.new options
-
end
-
-
1
def get_user_attributes_from_ldap_entry(entry)
-
{
-
:dn => entry.dn,
-
:firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
-
:lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
-
:mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
-
:auth_source_id => self.id
-
}
-
end
-
-
# Return the attributes needed for the LDAP search. It will only
-
# include the user attributes if on-the-fly registration is enabled
-
1
def search_attributes
-
if onthefly_register?
-
['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
-
else
-
['dn']
-
end
-
end
-
-
# Check if a DN (user record) authenticates with the password
-
1
def authenticate_dn(dn, password)
-
if dn.present? && password.present?
-
initialize_ldap_con(dn, password).bind
-
end
-
end
-
-
# Get the user's dn and any attributes for them, given their login
-
1
def get_user_dn(login, password)
-
ldap_con = nil
-
if self.account && self.account.include?("$login")
-
ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password)
-
else
-
ldap_con = initialize_ldap_con(self.account, self.account_password)
-
end
-
login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
-
object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
-
attrs = {}
-
-
search_filter = object_filter & login_filter
-
if f = ldap_filter
-
search_filter = search_filter & f
-
end
-
-
ldap_con.search( :base => self.base_dn,
-
:filter => search_filter,
-
:attributes=> search_attributes) do |entry|
-
-
if onthefly_register?
-
attrs = get_user_attributes_from_ldap_entry(entry)
-
else
-
attrs = {:dn => entry.dn}
-
end
-
-
logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
-
end
-
-
attrs
-
end
-
-
1
def self.get_attr(entry, attr_name)
-
if !attr_name.blank?
-
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Board < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :project
-
1
has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
-
1
has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC"
-
1
belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
-
1
acts_as_tree :dependent => :nullify
-
1
acts_as_list :scope => '(project_id = #{project_id} AND parent_id #{parent_id ? "= #{parent_id}" : "IS NULL"})'
-
1
acts_as_watchable
-
-
1
validates_presence_of :name, :description
-
1
validates_length_of :name, :maximum => 30
-
1
validates_length_of :description, :maximum => 255
-
1
validate :validate_board
-
-
1
scope :visible, lambda {|*args| { :include => :project,
-
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
-
-
1
safe_attributes 'name', 'description', 'parent_id', 'move_to'
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_messages, project)
-
end
-
-
1
def reload(*args)
-
@valid_parents = nil
-
super
-
end
-
-
1
def to_s
-
name
-
end
-
-
1
def valid_parents
-
@valid_parents ||= project.boards - self_and_descendants
-
end
-
-
1
def reset_counters!
-
self.class.reset_counters!(id)
-
end
-
-
# Updates topics_count, messages_count and last_message_id attributes for +board_id+
-
1
def self.reset_counters!(board_id)
-
board_id = board_id.to_i
-
update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
-
" messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
-
" last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
-
["id = ?", board_id])
-
end
-
-
1
def self.board_tree(boards, parent_id=nil, level=0)
-
21
tree = []
-
63
boards.select {|board| board.parent_id == parent_id}.sort_by(&:position).each do |board|
-
14
tree << [board, level]
-
14
tree += board_tree(boards, board.id, level+1)
-
end
-
21
if block_given?
-
7
tree.each do |board, level|
-
14
yield board, level
-
end
-
end
-
21
tree
-
end
-
-
1
protected
-
-
1
def validate_board
-
if parent_id && parent_id_changed?
-
errors.add(:parent_id, :invalid) unless valid_parents.include?(parent)
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Change < ActiveRecord::Base
-
1
belongs_to :changeset
-
-
1
validates_presence_of :changeset_id, :action, :path
-
1
before_save :init_path
-
1
before_validation :replace_invalid_utf8_of_path
-
-
1
def relative_path
-
changeset.repository.relative_path(path)
-
end
-
-
1
def replace_invalid_utf8_of_path
-
self.path = Redmine::CodesetUtil.replace_invalid_utf8(self.path)
-
self.from_path = Redmine::CodesetUtil.replace_invalid_utf8(self.from_path)
-
end
-
-
1
def init_path
-
self.path ||= ""
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'iconv'
-
-
1
class Changeset < ActiveRecord::Base
-
1
belongs_to :repository
-
1
belongs_to :user
-
1
has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
-
1
has_and_belongs_to_many :issues
-
1
has_and_belongs_to_many :parents,
-
:class_name => "Changeset",
-
:join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
-
:association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
-
1
has_and_belongs_to_many :children,
-
:class_name => "Changeset",
-
:join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
-
:association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
-
-
acts_as_event :title => Proc.new {|o| o.title},
-
:description => :long_comments,
-
:datetime => :committed_on,
-
1
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}}
-
-
acts_as_searchable :columns => 'comments',
-
:include => {:repository => :project},
-
:project_key => "#{Repository.table_name}.project_id",
-
1
:date_column => 'committed_on'
-
-
acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
-
:author_key => :user_id,
-
1
:find_options => {:include => [:user, {:repository => :project}]}
-
-
1
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
-
1
validates_uniqueness_of :revision, :scope => :repository_id
-
1
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
-
-
1
scope :visible,
-
lambda {|*args| { :include => {:repository => :project},
-
3
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
-
-
1
after_create :scan_for_issues
-
1
before_create :before_create_cs
-
-
1
def revision=(r)
-
write_attribute :revision, (r.nil? ? nil : r.to_s)
-
end
-
-
# Returns the identifier of this changeset; depending on repository backends
-
1
def identifier
-
if repository.class.respond_to? :changeset_identifier
-
repository.class.changeset_identifier self
-
else
-
revision.to_s
-
end
-
end
-
-
1
def committed_on=(date)
-
self.commit_date = date
-
super
-
end
-
-
# Returns the readable identifier
-
1
def format_identifier
-
if repository.class.respond_to? :format_changeset_identifier
-
repository.class.format_changeset_identifier self
-
else
-
identifier
-
end
-
end
-
-
1
def project
-
repository.project
-
end
-
-
1
def author
-
user || committer.to_s.split('<').first
-
end
-
-
1
def before_create_cs
-
self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
-
self.comments = self.class.normalize_comments(
-
self.comments, repository.repo_log_encoding)
-
self.user = repository.find_committer_user(self.committer)
-
end
-
-
1
def scan_for_issues
-
scan_comment_for_issue_ids
-
end
-
-
1
TIMELOG_RE = /
-
(
-
((\d+)(h|hours?))((\d+)(m|min)?)?
-
|
-
((\d+)(h|hours?|m|min))
-
|
-
(\d+):(\d+)
-
|
-
(\d+([\.,]\d+)?)h?
-
)
-
/x
-
-
1
def scan_comment_for_issue_ids
-
return if comments.blank?
-
# keywords used to reference issues
-
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
-
ref_keywords_any = ref_keywords.delete('*')
-
# keywords used to fix issues
-
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
-
-
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
-
-
referenced_issues = []
-
-
comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
-
action, refs = match[2], match[3]
-
next unless action.present? || ref_keywords_any
-
-
refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
-
issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
-
if issue
-
referenced_issues << issue
-
fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
-
log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
-
end
-
end
-
end
-
-
referenced_issues.uniq!
-
self.issues = referenced_issues unless referenced_issues.empty?
-
end
-
-
1
def short_comments
-
@short_comments || split_comments.first
-
end
-
-
1
def long_comments
-
@long_comments || split_comments.last
-
end
-
-
1
def text_tag(ref_project=nil)
-
tag = if scmid?
-
"commit:#{scmid}"
-
else
-
"r#{revision}"
-
end
-
if repository && repository.identifier.present?
-
tag = "#{repository.identifier}|#{tag}"
-
end
-
if ref_project && project && ref_project != project
-
tag = "#{project.identifier}:#{tag}"
-
end
-
tag
-
end
-
-
# Returns the title used for the changeset in the activity/search results
-
1
def title
-
repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
-
comm = short_comments.blank? ? '' : (': ' + short_comments)
-
"#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
-
end
-
-
# Returns the previous changeset
-
1
def previous
-
@previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
-
end
-
-
# Returns the next changeset
-
1
def next
-
@next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
-
end
-
-
# Creates a new Change from it's common parameters
-
1
def create_change(change)
-
Change.create(:changeset => self,
-
:action => change[:action],
-
:path => change[:path],
-
:from_path => change[:from_path],
-
:from_revision => change[:from_revision])
-
end
-
-
# Finds an issue that can be referenced by the commit message
-
1
def find_referenced_issue_by_id(id)
-
return nil if id.blank?
-
issue = Issue.find_by_id(id.to_i, :include => :project)
-
if Setting.commit_cross_project_ref?
-
# all issues can be referenced/fixed
-
elsif issue
-
# issue that belong to the repository project, a subproject or a parent project only
-
unless issue.project &&
-
(project == issue.project || project.is_ancestor_of?(issue.project) ||
-
project.is_descendant_of?(issue.project))
-
issue = nil
-
end
-
end
-
issue
-
end
-
-
1
private
-
-
1
def fix_issue(issue)
-
status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
-
if status.nil?
-
logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
-
return issue
-
end
-
-
# the issue may have been updated by the closure of another one (eg. duplicate)
-
issue.reload
-
# don't change the status is the issue is closed
-
return if issue.status && issue.status.is_closed?
-
-
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
-
issue.status = status
-
unless Setting.commit_fix_done_ratio.blank?
-
issue.done_ratio = Setting.commit_fix_done_ratio.to_i
-
end
-
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
-
{ :changeset => self, :issue => issue })
-
unless issue.save
-
logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
-
end
-
issue
-
end
-
-
1
def log_time(issue, hours)
-
time_entry = TimeEntry.new(
-
:user => user,
-
:hours => hours,
-
:issue => issue,
-
:spent_on => commit_date,
-
:comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
-
:locale => Setting.default_language)
-
)
-
time_entry.activity = log_time_activity unless log_time_activity.nil?
-
-
unless time_entry.save
-
logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
-
end
-
time_entry
-
end
-
-
1
def log_time_activity
-
if Setting.commit_logtime_activity_id.to_i > 0
-
TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
-
end
-
end
-
-
1
def split_comments
-
comments =~ /\A(.+?)\r?\n(.*)$/m
-
@short_comments = $1 || comments
-
@long_comments = $2.to_s.strip
-
return @short_comments, @long_comments
-
end
-
-
1
public
-
-
# Strips and reencodes a commit log before insertion into the database
-
1
def self.normalize_comments(str, encoding)
-
Changeset.to_utf8(str.to_s.strip, encoding)
-
end
-
-
1
def self.to_utf8(str, encoding)
-
Redmine::CodesetUtil.to_utf8(str, encoding)
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Comment < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :commented, :polymorphic => true, :counter_cache => true
-
1
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
-
-
1
validates_presence_of :commented, :author, :comments
-
-
1
safe_attributes 'comments'
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CommentObserver < ActiveRecord::Observer
-
1
def after_create(comment)
-
if comment.commented.is_a?(News) && Setting.notified_events.include?('news_comment_added')
-
Mailer.news_comment_added(comment).deliver
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CustomField < ActiveRecord::Base
-
1
include Redmine::SubclassFactory
-
-
1
has_many :custom_values, :dependent => :delete_all
-
1
acts_as_list :scope => 'type = \'#{self.class}\''
-
1
serialize :possible_values
-
-
1
validates_presence_of :name, :field_format
-
1
validates_uniqueness_of :name, :scope => :type
-
1
validates_length_of :name, :maximum => 30
-
1
validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
-
-
1
validate :validate_custom_field
-
1
before_validation :set_searchable
-
-
1
CUSTOM_FIELDS_TABS = [
-
{:name => 'IssueCustomField', :partial => 'custom_fields/index',
-
:label => :label_issue_plural},
-
{:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
-
:label => :label_spent_time},
-
{:name => 'ProjectCustomField', :partial => 'custom_fields/index',
-
:label => :label_project_plural},
-
{:name => 'VersionCustomField', :partial => 'custom_fields/index',
-
:label => :label_version_plural},
-
{:name => 'UserCustomField', :partial => 'custom_fields/index',
-
:label => :label_user_plural},
-
{:name => 'GroupCustomField', :partial => 'custom_fields/index',
-
:label => :label_group_plural},
-
{:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
-
:label => TimeEntryActivity::OptionName},
-
{:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
-
:label => IssuePriority::OptionName},
-
{:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
-
:label => DocumentCategory::OptionName}
-
]
-
-
10
CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]}
-
-
1
def field_format=(arg)
-
# cannot change format of a saved custom field
-
super if new_record?
-
end
-
-
1
def set_searchable
-
# make sure these fields are not searchable
-
self.searchable = false if %w(int float date bool).include?(field_format)
-
# make sure only these fields can have multiple values
-
self.multiple = false unless %w(list user version).include?(field_format)
-
true
-
end
-
-
1
def validate_custom_field
-
if self.field_format == "list"
-
errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
-
errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
-
end
-
-
if regexp.present?
-
begin
-
Regexp.new(regexp)
-
rescue
-
errors.add(:regexp, :invalid)
-
end
-
end
-
-
if default_value.present? && !valid_field_value?(default_value)
-
errors.add(:default_value, :invalid)
-
end
-
end
-
-
1
def possible_values_options(obj=nil)
-
7
case field_format
-
when 'user', 'version'
-
if obj.respond_to?(:project) && obj.project
-
case field_format
-
when 'user'
-
obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
-
when 'version'
-
obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
-
end
-
elsif obj.is_a?(Array)
-
obj.collect {|o| possible_values_options(o)}.reduce(:&)
-
else
-
[]
-
end
-
when 'bool'
-
[[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
-
else
-
7
possible_values || []
-
end
-
end
-
-
1
def possible_values(obj=nil)
-
59
case field_format
-
when 'user', 'version'
-
possible_values_options(obj).collect(&:last)
-
when 'bool'
-
['1', '0']
-
else
-
59
values = super()
-
59
if values.is_a?(Array)
-
59
values.each do |value|
-
210
value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
-
end
-
end
-
59
values || []
-
end
-
end
-
-
# Makes possible_values accept a multiline string
-
1
def possible_values=(arg)
-
if arg.is_a?(Array)
-
super(arg.compact.collect(&:strip).select {|v| !v.blank?})
-
else
-
self.possible_values = arg.to_s.split(/[\n\r]+/)
-
end
-
end
-
-
1
def cast_value(value)
-
casted = nil
-
unless value.blank?
-
case field_format
-
when 'string', 'text', 'list'
-
casted = value
-
when 'date'
-
casted = begin; value.to_date; rescue; nil end
-
when 'bool'
-
casted = (value == '1' ? true : false)
-
when 'int'
-
casted = value.to_i
-
when 'float'
-
casted = value.to_f
-
when 'user', 'version'
-
casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
-
end
-
end
-
casted
-
end
-
-
1
def value_from_keyword(keyword, customized)
-
possible_values_options = possible_values_options(customized)
-
if possible_values_options.present?
-
keyword = keyword.to_s.downcase
-
if v = possible_values_options.detect {|text, id| text.downcase == keyword}
-
if v.is_a?(Array)
-
v.last
-
else
-
v
-
end
-
end
-
else
-
keyword
-
end
-
end
-
-
# Returns a ORDER BY clause that can used to sort customized
-
# objects by their value of the custom field.
-
# Returns nil if the custom field can not be used for sorting.
-
1
def order_statement
-
200
return nil if multiple?
-
200
case field_format
-
when 'string', 'text', 'list', 'date', 'bool'
-
# COALESCE is here to make sure that blank and NULL values are sorted equally
-
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
-
" WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
-
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
-
175
" AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
-
when 'int', 'float'
-
# Make the database cast values into numeric
-
# Postgresql will raise an error if a value can not be casted!
-
# CustomValue validations should ensure that it doesn't occur
-
"(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
-
" WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
-
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
-
25
" AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
-
when 'user', 'version'
-
value_class.fields_for_order_statement(value_join_alias)
-
else
-
nil
-
end
-
end
-
-
# Returns a GROUP BY clause that can used to group by custom value
-
# Returns nil if the custom field can not be used for grouping.
-
1
def group_statement
-
125
return nil if multiple?
-
125
case field_format
-
when 'list', 'date', 'bool', 'int'
-
75
order_statement
-
when 'user', 'version'
-
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
-
" WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
-
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
-
" AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
-
else
-
nil
-
end
-
end
-
-
1
def join_for_order_statement
-
case field_format
-
when 'user', 'version'
-
"LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
-
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
-
" AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
-
" AND #{join_alias}.custom_field_id = #{id}" +
-
" AND #{join_alias}.value <> ''" +
-
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
-
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
-
" AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
-
" AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
-
" LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
-
" ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id"
-
else
-
nil
-
end
-
end
-
-
1
def join_alias
-
"cf_#{id}"
-
end
-
-
1
def value_join_alias
-
join_alias + "_" + field_format
-
end
-
-
1
def <=>(field)
-
8754
position <=> field.position
-
end
-
-
# Returns the class that values represent
-
1
def value_class
-
case field_format
-
when 'user', 'version'
-
field_format.classify.constantize
-
else
-
nil
-
end
-
end
-
-
1
def self.customized_class
-
712
self.name =~ /^(.+)CustomField$/
-
712
begin; $1.constantize; rescue nil; end
-
end
-
-
# to move in project_custom_field
-
1
def self.for_all
-
1533
find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
-
end
-
-
1
def type_name
-
nil
-
end
-
-
# Returns the error messages for the given value
-
# or an empty array if value is a valid value for the custom field
-
1
def validate_field_value(value)
-
34
errs = []
-
34
if value.is_a?(Array)
-
if !multiple?
-
errs << ::I18n.t('activerecord.errors.messages.invalid')
-
end
-
if is_required? && value.detect(&:present?).nil?
-
errs << ::I18n.t('activerecord.errors.messages.blank')
-
end
-
value.each {|v| errs += validate_field_value_format(v)}
-
else
-
34
if is_required? && value.blank?
-
errs << ::I18n.t('activerecord.errors.messages.blank')
-
end
-
34
errs += validate_field_value_format(value)
-
end
-
34
errs
-
end
-
-
# Returns true if value is a valid value for the custom field
-
1
def valid_field_value?(value)
-
validate_field_value(value).empty?
-
end
-
-
1
def format_in?(*args)
-
args.include?(field_format)
-
end
-
-
1
protected
-
-
# Returns the error message for the given value regarding its format
-
1
def validate_field_value_format(value)
-
34
errs = []
-
34
if value.present?
-
2
errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
-
2
errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
-
2
errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
-
-
# Format specific validations
-
2
case field_format
-
when 'int'
-
errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
-
when 'float'
-
begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
-
when 'date'
-
errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
-
when 'list'
-
errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
-
end
-
end
-
34
errs
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CustomFieldValue
-
1
attr_accessor :custom_field, :customized, :value
-
-
1
def custom_field_id
-
2
custom_field.id
-
end
-
-
1
def true?
-
33
self.value == '1'
-
end
-
-
1
def editable?
-
custom_field.editable?
-
end
-
-
1
def visible?
-
62
custom_field.visible?
-
end
-
-
1
def required?
-
custom_field.is_required?
-
end
-
-
1
def to_s
-
value.to_s
-
end
-
-
1
def validate_value
-
34
custom_field.validate_field_value(value).each do |message|
-
customized.errors.add(:base, custom_field.name + ' ' + message)
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CustomValue < ActiveRecord::Base
-
1
belongs_to :custom_field
-
1
belongs_to :customized, :polymorphic => true
-
-
1
def initialize(attributes=nil, *args)
-
210
super
-
210
if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?))
-
39
self.value ||= custom_field.default_value
-
end
-
end
-
-
# Returns true if the boolean custom value is true
-
1
def true?
-
self.value == '1'
-
end
-
-
1
def editable?
-
custom_field.editable?
-
end
-
-
1
def visible?
-
custom_field.visible?
-
end
-
-
1
def required?
-
custom_field.is_required?
-
end
-
-
1
def to_s
-
value.to_s
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Document < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :project
-
1
belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id"
-
1
acts_as_attachable :delete_permission => :manage_documents
-
-
1
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
-
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
-
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
-
1
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
-
1
acts_as_activity_provider :find_options => {:include => :project}
-
-
1
validates_presence_of :project, :title, :category
-
1
validates_length_of :title, :maximum => 60
-
-
1
scope :visible, lambda {|*args| { :include => :project,
-
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_documents, *args) } }
-
-
1
safe_attributes 'category_id', 'title', 'description'
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_documents, project)
-
end
-
-
1
def initialize(attributes=nil, *args)
-
super
-
if new_record?
-
self.category ||= DocumentCategory.default
-
end
-
end
-
-
1
def updated_on
-
unless @updated_on
-
a = attachments.last
-
@updated_on = (a && a.created_on) || created_on
-
end
-
@updated_on
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class DocumentCategory < Enumeration
-
1
has_many :documents, :foreign_key => 'category_id'
-
-
1
OptionName = :enumeration_doc_categories
-
-
1
def option_name
-
OptionName
-
end
-
-
1
def objects_count
-
documents.count
-
end
-
-
1
def transfer_relations(to)
-
documents.update_all("category_id = #{to.id}")
-
end
-
-
1
def self.default
-
d = super
-
d = first if d.nil?
-
d
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class DocumentCategoryCustomField < CustomField
-
1
def type_name
-
:enumeration_doc_categories
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class DocumentObserver < ActiveRecord::Observer
-
1
def after_create(document)
-
Mailer.document_added(document).deliver if Setting.notified_events.include?('document_added')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class EnabledModule < ActiveRecord::Base
-
1
belongs_to :project
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name, :scope => :project_id
-
-
1
after_create :module_enabled
-
-
1
private
-
-
# after_create callback used to do things when a module is enabled
-
1
def module_enabled
-
508
case name
-
when 'wiki'
-
# Create a wiki with a default start page
-
32
if project && project.wiki.nil?
-
32
Wiki.create(:project => project, :start_page => 'Wiki')
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Enumeration < ActiveRecord::Base
-
1
include Redmine::SubclassFactory
-
-
1
default_scope :order => "#{Enumeration.table_name}.position ASC"
-
-
1
belongs_to :project
-
-
1
acts_as_list :scope => 'type = \'#{type}\''
-
1
acts_as_customizable
-
1
acts_as_tree :order => 'position ASC'
-
-
1
before_destroy :check_integrity
-
1
before_save :check_default
-
-
1
attr_protected :type
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name, :scope => [:type, :project_id]
-
1
validates_length_of :name, :maximum => 30
-
-
1
scope :shared, where(:project_id => nil)
-
1
scope :sorted, order("#{table_name}.position ASC")
-
1
scope :active, where(:active => true)
-
1
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
-
-
1
def self.default
-
# Creates a fake default scope so Enumeration.default will check
-
# it's type. STI subclasses will automatically add their own
-
# types to the finder.
-
5898
if self.descends_from_active_record?
-
where(:is_default => true, :type => 'Enumeration').first
-
else
-
# STI classes are
-
5898
where(:is_default => true).first
-
end
-
end
-
-
# Overloaded on concrete classes
-
1
def option_name
-
nil
-
end
-
-
1
def check_default
-
if is_default? && is_default_changed?
-
Enumeration.update_all({:is_default => false}, {:type => type})
-
end
-
end
-
-
# Overloaded on concrete classes
-
1
def objects_count
-
0
-
end
-
-
1
def in_use?
-
self.objects_count != 0
-
end
-
-
# Is this enumeration overiding a system level enumeration?
-
1
def is_override?
-
!self.parent.nil?
-
end
-
-
1
alias :destroy_without_reassign :destroy
-
-
# Destroy the enumeration
-
# If a enumeration is specified, objects are reassigned
-
1
def destroy(reassign_to = nil)
-
if reassign_to && reassign_to.is_a?(Enumeration)
-
self.transfer_relations(reassign_to)
-
end
-
destroy_without_reassign
-
end
-
-
1
def <=>(enumeration)
-
position <=> enumeration.position
-
end
-
-
2137
def to_s; name end
-
-
# Returns the Subclasses of Enumeration. Each Subclass needs to be
-
# required in development mode.
-
#
-
# Note: subclasses is protected in ActiveRecord
-
1
def self.get_subclasses
-
subclasses
-
end
-
-
# Does the +new+ Hash override the previous Enumeration?
-
1
def self.overridding_change?(new, previous)
-
if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous)
-
return false
-
else
-
return true
-
end
-
end
-
-
# Does the +new+ Hash have the same custom values as the previous Enumeration?
-
1
def self.same_custom_values?(new, previous)
-
previous.custom_field_values.each do |custom_value|
-
if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s]
-
return false
-
end
-
end
-
-
return true
-
end
-
-
# Are the new and previous fields equal?
-
1
def self.same_active_state?(new, previous)
-
new = (new == "1" ? true : false)
-
return new == previous
-
end
-
-
1
private
-
1
def check_integrity
-
raise "Can't delete enumeration" if self.in_use?
-
end
-
-
end
-
-
# Force load the subclasses in development mode
-
1
require_dependency 'time_entry_activity'
-
1
require_dependency 'document_category'
-
1
require_dependency 'issue_priority'
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Group < Principal
-
1
include Redmine::SafeAttributes
-
-
1
has_and_belongs_to_many :users, :after_add => :user_added,
-
:after_remove => :user_removed
-
-
1
acts_as_customizable
-
-
1
validates_presence_of :lastname
-
1
validates_uniqueness_of :lastname, :case_sensitive => false
-
1
validates_length_of :lastname, :maximum => 30
-
-
1
before_destroy :remove_references_before_destroy
-
-
1
scope :sorted, order("#{table_name}.lastname ASC")
-
-
1
safe_attributes 'name',
-
'user_ids',
-
'custom_field_values',
-
'custom_fields',
-
:if => lambda {|group, user| user.admin?}
-
-
1
def to_s
-
28
lastname.to_s
-
end
-
-
1
def name
-
52
lastname
-
end
-
-
1
def name=(arg)
-
self.lastname = arg
-
end
-
-
1
def user_added(user)
-
members.each do |member|
-
next if member.project.nil?
-
user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
-
member.member_roles.each do |member_role|
-
user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
-
end
-
user_member.save!
-
end
-
end
-
-
1
def user_removed(user)
-
members.each do |member|
-
MemberRole.find(:all, :include => :member,
-
:conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
-
end
-
end
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == 'lastname'
-
attr_name = "name"
-
end
-
super(attr_name, *args)
-
end
-
-
1
private
-
-
# Removes references that are not handled by associations
-
1
def remove_references_before_destroy
-
return if self.id.nil?
-
-
Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class GroupCustomField < CustomField
-
1
def type_name
-
:label_group_plural
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Issue < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
include Redmine::Utils::DateCalculation
-
-
1
belongs_to :project
-
1
belongs_to :tracker
-
1
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
-
1
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
-
1
belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
-
1
belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
-
1
belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
-
1
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
-
-
1
has_many :journals, :as => :journalized, :dependent => :destroy
-
1
has_many :visible_journals,
-
:class_name => 'Journal',
-
:as => :journalized,
-
:conditions => Proc.new {
-
["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
-
},
-
:readonly => true
-
-
1
has_many :time_entries, :dependent => :delete_all
-
1
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
-
-
1
has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
-
1
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
-
-
1
acts_as_nested_set :scope => 'root_id', :dependent => :destroy
-
1
acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
-
1
acts_as_customizable
-
1
acts_as_watchable
-
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
-
:include => [:project, :visible_journals],
-
# sort by id so that limited eager loading doesn't break with postgresql
-
1
:order_column => "#{table_name}.id"
-
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
-
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
-
1
:type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
-
-
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
-
1
:author_key => :author_id
-
-
1
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
-
-
1
attr_reader :current_journal
-
1
delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
-
-
1
validates_presence_of :subject, :priority, :project, :tracker, :author, :status
-
-
1
validates_length_of :subject, :maximum => 255
-
1
validates_inclusion_of :done_ratio, :in => 0..100
-
1
validates_numericality_of :estimated_hours, :allow_nil => true
-
1
validate :validate_issue, :validate_required_fields
-
-
1
scope :visible,
-
lambda {|*args| { :include => :project,
-
1121
:conditions => Issue.visible_condition(args.shift || User.current, *args) } }
-
-
1
scope :open, lambda {|*args|
-
315
is_closed = args.size > 0 ? !args.first : false
-
315
{:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
-
}
-
-
1
scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
-
1
scope :on_active_project, :include => [:status, :project, :tracker],
-
:conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
-
-
1
before_create :default_assign
-
1
before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
-
1582
after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
-
1
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
-
# Should be after_create but would be called before previous after_save callbacks
-
1
after_save :after_create_from_copy
-
1
after_destroy :update_parent_attributes
-
-
# Returns a SQL conditions string used to find all issues visible by the specified user
-
1
def self.visible_condition(user, options={})
-
1121
Project.allowed_to_condition(user, :view_issues, options) do |role, user|
-
3354
if user.logged?
-
3354
case role.issues_visibility
-
when 'all'
-
1118
nil
-
when 'default'
-
2236
user_ids = [user.id] + user.groups.map(&:id)
-
2236
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
-
when 'own'
-
user_ids = [user.id] + user.groups.map(&:id)
-
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
-
else
-
'1=0'
-
end
-
else
-
"(#{table_name}.is_private = #{connection.quoted_false})"
-
end
-
end
-
end
-
-
# Returns true if usr or current user is allowed to view the issue
-
1
def visible?(usr=nil)
-
1828
(usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
-
1822
if user.logged?
-
1822
case role.issues_visibility
-
when 'all'
-
1013
true
-
when 'default'
-
809
!self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
-
when 'own'
-
self.author == user || user.is_or_belongs_to?(assigned_to)
-
else
-
false
-
end
-
else
-
!self.is_private?
-
end
-
end
-
end
-
-
1
def initialize(attributes=nil, *args)
-
2401
super
-
2401
if new_record?
-
# set default values for new records only
-
2401
self.status ||= IssueStatus.default
-
2401
self.priority ||= IssuePriority.default
-
2401
self.watcher_user_ids = []
-
end
-
end
-
-
# AR#Persistence#destroy would raise and RecordNotFound exception
-
# if the issue was already deleted or updated (non matching lock_version).
-
# This is a problem when bulk deleting issues or deleting a project
-
# (because an issue may already be deleted if its parent was deleted
-
# first).
-
# The issue is reloaded by the nested_set before being deleted so
-
# the lock_version condition should not be an issue but we handle it.
-
1
def destroy
-
1163
super
-
rescue ActiveRecord::RecordNotFound
-
# Stale or already deleted
-
begin
-
reload
-
rescue ActiveRecord::RecordNotFound
-
# The issue was actually already deleted
-
@destroyed = true
-
return freeze
-
end
-
# The issue was stale, retry to destroy
-
super
-
end
-
-
1
def reload(*args)
-
4036
@workflow_rule_by_attribute = nil
-
4036
@assignable_versions = nil
-
4036
super
-
end
-
-
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
-
1
def available_custom_fields
-
1598
(project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
-
end
-
-
# Copies attributes from another issue, arg can be an id or an Issue
-
1
def copy_from(arg, options={})
-
8
issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
-
8
self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
-
8
self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
-
8
self.status = issue.status
-
8
self.author = User.current
-
8
unless options[:attachments] == false
-
4
self.attachments = issue.attachments.map do |attachement|
-
attachement.copy(:container => self)
-
end
-
end
-
8
@copied_from = issue
-
8
@copy_options = options
-
8
self
-
end
-
-
# Returns an unsaved copy of the issue
-
1
def copy(attributes=nil, copy_options={})
-
copy = self.class.new.copy_from(self, copy_options)
-
copy.attributes = attributes if attributes
-
copy
-
end
-
-
# Returns true if the issue is a copy
-
1
def copy?
-
1816
@copied_from.present?
-
end
-
-
# Moves/copies an issue to a new project and tracker
-
# Returns the moved/copied issue on success, false on failure
-
1
def move_to_project(new_project, new_tracker=nil, options={})
-
ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
-
-
if options[:copy]
-
issue = self.copy
-
else
-
issue = self
-
end
-
-
issue.init_journal(User.current, options[:notes])
-
-
# Preserve previous behaviour
-
# #move_to_project doesn't change tracker automatically
-
issue.send :project=, new_project, true
-
if new_tracker
-
issue.tracker = new_tracker
-
end
-
# Allow bulk setting of attributes on the issue
-
if options[:attributes]
-
issue.attributes = options[:attributes]
-
end
-
-
issue.save ? issue : false
-
end
-
-
1
def status_id=(sid)
-
983
self.status = nil
-
983
result = write_attribute(:status_id, sid)
-
983
@workflow_rule_by_attribute = nil
-
983
result
-
end
-
-
1
def priority_id=(pid)
-
903
self.priority = nil
-
903
write_attribute(:priority_id, pid)
-
end
-
-
1
def category_id=(cid)
-
903
self.category = nil
-
903
write_attribute(:category_id, cid)
-
end
-
-
1
def fixed_version_id=(vid)
-
904
self.fixed_version = nil
-
904
write_attribute(:fixed_version_id, vid)
-
end
-
-
1
def tracker_id=(tid)
-
904
self.tracker = nil
-
904
result = write_attribute(:tracker_id, tid)
-
904
@custom_field_values = nil
-
904
@workflow_rule_by_attribute = nil
-
904
result
-
end
-
-
1
def project_id=(project_id)
-
1095
if project_id.to_s != self.project_id.to_s
-
1093
self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
-
end
-
end
-
-
1
def project=(project, keep_tracker=false)
-
1103
project_was = self.project
-
1103
write_attribute(:project_id, project ? project.id : nil)
-
1103
association_instance_set('project', project)
-
1103
if project_was && project && project_was != project
-
@assignable_versions = nil
-
-
unless keep_tracker || project.trackers.include?(tracker)
-
self.tracker = project.trackers.first
-
end
-
# Reassign to the category with same name if any
-
if category
-
self.category = project.issue_categories.find_by_name(category.name)
-
end
-
# Keep the fixed_version if it's still valid in the new_project
-
if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
-
self.fixed_version = nil
-
end
-
# Clear the parent task if it's no longer valid
-
unless valid_parent_project?
-
self.parent_issue_id = nil
-
end
-
@custom_field_values = nil
-
end
-
end
-
-
1
def description=(arg)
-
903
if arg.is_a?(String)
-
2
arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
-
end
-
903
write_attribute(:description, arg)
-
end
-
-
# Overrides assign_attributes so that project and tracker get assigned first
-
1
def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
-
1172
return if new_attributes.nil?
-
1172
attrs = new_attributes.dup
-
1172
attrs.stringify_keys!
-
-
1172
%w(project project_id tracker tracker_id).each do |attr|
-
4688
if attrs.has_key?(attr)
-
1995
send "#{attr}=", attrs.delete(attr)
-
end
-
end
-
1172
send :assign_attributes_without_project_and_tracker_first, attrs, *args
-
end
-
# Do not redefine alias chain on reload (see #4838)
-
1
alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
-
-
1
def estimated_hours=(h)
-
1568
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
-
end
-
-
1
safe_attributes 'project_id',
-
:if => lambda {|issue, user|
-
238
if issue.new_record?
-
235
issue.copy?
-
elsif user.allowed_to?(:move_issues, issue.project)
-
3
projects = Issue.allowed_target_projects_on_move(user)
-
3
projects.include?(issue.project) && projects.size > 1
-
end
-
}
-
-
1
safe_attributes 'tracker_id',
-
'status_id',
-
'category_id',
-
'assigned_to_id',
-
'priority_id',
-
'fixed_version_id',
-
'subject',
-
'description',
-
'start_date',
-
'due_date',
-
'done_ratio',
-
'estimated_hours',
-
'custom_field_values',
-
'custom_fields',
-
'lock_version',
-
'notes',
-
238
:if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
-
-
1
safe_attributes 'status_id',
-
'assigned_to_id',
-
'fixed_version_id',
-
'done_ratio',
-
'lock_version',
-
'notes',
-
238
:if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
-
-
1
safe_attributes 'notes',
-
238
:if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
-
-
1
safe_attributes 'private_notes',
-
238
:if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
-
-
1
safe_attributes 'watcher_user_ids',
-
238
:if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
-
-
1
safe_attributes 'is_private',
-
:if => lambda {|issue, user|
-
user.allowed_to?(:set_issues_private, issue.project) ||
-
238
(issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
-
}
-
-
1
safe_attributes 'parent_issue_id',
-
238
:if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
-
238
user.allowed_to?(:manage_subtasks, issue.project)}
-
-
1
def safe_attribute_names(user=nil)
-
327
names = super
-
327
names -= disabled_core_fields
-
327
names -= read_only_attribute_names(user)
-
327
names
-
end
-
-
# Safely sets attributes
-
# Should be called from controllers instead of #attributes=
-
# attr_accessible is too rough because we still want things like
-
# Issue.new(:project => foo) to work
-
1
def safe_attributes=(attrs, user=User.current)
-
4
return unless attrs.is_a?(Hash)
-
-
2
attrs = attrs.dup
-
-
# Project and Tracker must be set before since new_statuses_allowed_to depends on it.
-
2
if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
-
2
if allowed_target_projects(user).collect(&:id).include?(p.to_i)
-
2
self.project_id = p
-
end
-
end
-
-
2
if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
-
2
self.tracker_id = t
-
end
-
-
2
if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
-
2
if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
-
2
self.status_id = s
-
end
-
end
-
-
2
attrs = delete_unsafe_attributes(attrs, user)
-
2
return if attrs.empty?
-
-
2
unless leaf?
-
attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
-
end
-
-
2
if attrs['parent_issue_id'].present?
-
s = attrs['parent_issue_id'].to_s
-
unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
-
@invalid_parent_issue_id = attrs.delete('parent_issue_id')
-
end
-
end
-
-
2
if attrs['custom_field_values'].present?
-
attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
-
end
-
-
2
if attrs['custom_fields'].present?
-
attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
-
end
-
-
# mass-assignment security bypass
-
2
assign_attributes attrs, :without_protection => true
-
end
-
-
1
def disabled_core_fields
-
348
tracker ? tracker.disabled_core_fields : []
-
end
-
-
# Returns the custom_field_values that can be edited by the given user
-
1
def editable_custom_field_values(user=nil)
-
5
custom_field_values.reject do |value|
-
read_only_attribute_names(user).include?(value.custom_field_id.to_s)
-
end
-
end
-
-
# Returns the names of attributes that are read-only for user or the current user
-
# For users with multiple roles, the read-only fields are the intersection of
-
# read-only fields of each role
-
# The result is an array of strings where sustom fields are represented with their ids
-
#
-
# Examples:
-
# issue.read_only_attribute_names # => ['due_date', '2']
-
# issue.read_only_attribute_names(user) # => []
-
1
def read_only_attribute_names(user=nil)
-
327
workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
-
end
-
-
# Returns the names of required attributes for user or the current user
-
# For users with multiple roles, the required fields are the intersection of
-
# required fields of each role
-
# The result is an array of strings where sustom fields are represented with their ids
-
#
-
# Examples:
-
# issue.required_attribute_names # => ['due_date', '2']
-
# issue.required_attribute_names(user) # => []
-
1
def required_attribute_names(user=nil)
-
1230
workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
-
end
-
-
# Returns true if the attribute is required for user
-
1
def required_attribute?(name, user=nil)
-
42
required_attribute_names(user).include?(name.to_s)
-
end
-
-
# Returns a hash of the workflow rule by attribute for the given user
-
#
-
# Examples:
-
# issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
-
1
def workflow_rule_by_attribute(user=nil)
-
1557
return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
-
-
1428
user_real = user || User.current
-
1428
roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
-
1428
return {} if roles.empty?
-
-
1428
result = {}
-
1428
workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
-
1428
if workflow_permissions.any?
-
workflow_rules = workflow_permissions.inject({}) do |h, wp|
-
h[wp.field_name] ||= []
-
h[wp.field_name] << wp.rule
-
h
-
end
-
workflow_rules.each do |attr, rules|
-
next if rules.size < roles.size
-
uniq_rules = rules.uniq
-
if uniq_rules.size == 1
-
result[attr] = uniq_rules.first
-
else
-
result[attr] = 'required'
-
end
-
end
-
end
-
1428
@workflow_rule_by_attribute = result if user.nil?
-
1428
result
-
end
-
1
private :workflow_rule_by_attribute
-
-
1
def done_ratio
-
1443
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
-
46
status.default_done_ratio
-
else
-
1397
read_attribute(:done_ratio)
-
end
-
end
-
-
1
def self.use_status_for_done_ratio?
-
3432
Setting.issue_done_ratio == 'issue_status'
-
end
-
-
1
def self.use_field_for_done_ratio?
-
2
Setting.issue_done_ratio == 'issue_field'
-
end
-
-
1
def validate_issue
-
1188
if due_date.nil? && @attributes['due_date'].present?
-
errors.add :due_date, :not_a_date
-
end
-
-
1188
if start_date.nil? && @attributes['start_date'].present?
-
errors.add :start_date, :not_a_date
-
end
-
-
1188
if due_date && start_date && due_date < start_date
-
errors.add :due_date, :greater_than_start_date
-
end
-
-
1188
if start_date && soonest_start && start_date < soonest_start
-
errors.add :start_date, :invalid
-
end
-
-
1188
if fixed_version
-
734
if !assignable_versions.include?(fixed_version)
-
errors.add :fixed_version_id, :inclusion
-
elsif reopened? && fixed_version.closed?
-
errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
-
end
-
end
-
-
# Checks that the issue can not be added/moved to a disabled tracker
-
1188
if project && (tracker_id_changed? || project_id_changed?)
-
862
unless project.trackers.include?(tracker)
-
errors.add :tracker_id, :inclusion
-
end
-
end
-
-
# Checks parent issue assignment
-
1188
if @invalid_parent_issue_id.present?
-
errors.add :parent_issue_id, :invalid
-
1188
elsif @parent_issue
-
236
if !valid_parent_project?(@parent_issue)
-
errors.add :parent_issue_id, :invalid
-
236
elsif !new_record?
-
# moving an existing issue
-
118
if @parent_issue.root_id != root_id
-
# we can always move to another tree
-
109
elsif move_possible?(@parent_issue)
-
# move accepted inside tree
-
else
-
errors.add :parent_issue_id, :invalid
-
end
-
end
-
end
-
end
-
-
# Validates the issue against additional workflow requirements
-
1
def validate_required_fields
-
1188
user = new_record? ? author : current_journal.try(:user)
-
-
1188
required_attribute_names(user).each do |attribute|
-
if attribute =~ /^\d+$/
-
attribute = attribute.to_i
-
v = custom_field_values.detect {|v| v.custom_field_id == attribute }
-
if v && v.value.blank?
-
errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
-
end
-
else
-
if respond_to?(attribute) && send(attribute).blank?
-
errors.add attribute, :blank
-
end
-
end
-
end
-
end
-
-
# Set the done_ratio using the status if that setting is set. This will keep the done_ratios
-
# even if the user turns off the setting later
-
1
def update_done_ratio_from_issue_status
-
1581
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
-
37
self.done_ratio = status.default_done_ratio
-
end
-
end
-
-
1
def init_journal(user, notes = "")
-
381
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
-
381
if new_record?
-
@current_journal.notify = false
-
else
-
381
@attributes_before_change = attributes.dup
-
381
@custom_values_before_change = {}
-
381
self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
-
end
-
381
@current_journal
-
end
-
-
# Returns the id of the last journal or nil
-
1
def last_journal_id
-
3
if new_record?
-
nil
-
else
-
3
journals.maximum(:id)
-
end
-
end
-
-
# Returns a scope for journals that have an id greater than journal_id
-
1
def journals_after(journal_id)
-
scope = journals.reorder("#{Journal.table_name}.id ASC")
-
if journal_id.present?
-
scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
-
end
-
scope
-
end
-
-
# Return true if the issue is closed, otherwise false
-
1
def closed?
-
904
self.status.is_closed?
-
end
-
-
# Return true if the issue is being reopened
-
1
def reopened?
-
734
if !new_record? && status_id_changed?
-
91
status_was = IssueStatus.find_by_id(status_id_was)
-
91
status_new = IssueStatus.find_by_id(status_id)
-
91
if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
-
return true
-
end
-
end
-
734
false
-
end
-
-
# Return true if the issue is being closed
-
1
def closing?
-
1581
if !new_record? && status_id_changed?
-
92
status_was = IssueStatus.find_by_id(status_id_was)
-
92
status_new = IssueStatus.find_by_id(status_id)
-
92
if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
-
41
return true
-
end
-
end
-
1540
false
-
end
-
-
# Returns true if the issue is overdue
-
1
def overdue?
-
726
!due_date.nil? && (due_date < Date.today) && !status.is_closed?
-
end
-
-
# Is the amount of work done less than it should for the due date
-
1
def behind_schedule?
-
return false if start_date.nil? || due_date.nil?
-
done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
-
return done_date <= Date.today
-
end
-
-
# Does this issue have children?
-
1
def children?
-
!leaf?
-
end
-
-
# Users the issue can be assigned to
-
1
def assignable_users
-
5
users = project.assignable_users
-
5
users << author if author
-
5
users << assigned_to if assigned_to
-
5
users.uniq.sort
-
end
-
-
# Versions that the issue can be assigned to
-
1
def assignable_versions
-
744
return @assignable_versions if @assignable_versions
-
-
739
versions = project.shared_versions.open.all
-
739
if fixed_version
-
739
if fixed_version_id_changed?
-
# nothing to do
-
elsif project_id_changed?
-
if project.shared_versions.include?(fixed_version)
-
versions << fixed_version
-
end
-
else
-
290
versions << fixed_version
-
end
-
end
-
739
@assignable_versions = versions.uniq.sort
-
end
-
-
# Returns true if this issue is blocked by another issue that is still open
-
1
def blocked?
-
271
!relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
-
end
-
-
# Returns an array of statuses that user is able to apply
-
1
def new_statuses_allowed_to(user=User.current, include_default=false)
-
263
if new_record? && @copied_from
-
12
[IssueStatus.default, @copied_from.status].compact.uniq.sort
-
else
-
251
initial_status = nil
-
251
if new_record?
-
229
initial_status = IssueStatus.default
-
elsif status_id_was
-
22
initial_status = IssueStatus.find_by_id(status_id_was)
-
end
-
251
initial_status ||= status
-
-
251
statuses = initial_status.find_new_statuses_allowed_to(
-
user.admin ? Role.all : user.roles_for_project(project),
-
tracker,
-
author == user,
-
assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
-
)
-
251
statuses << initial_status unless statuses.empty?
-
251
statuses << IssueStatus.default if include_default
-
251
statuses = statuses.compact.uniq.sort
-
347
blocked? ? statuses.reject {|s| s.is_closed?} : statuses
-
end
-
end
-
-
1
def assigned_to_was
-
1003
if assigned_to_id_changed? && assigned_to_id_was.present?
-
@assigned_to_was ||= User.find_by_id(assigned_to_id_was)
-
end
-
end
-
-
# Returns the users that should be notified
-
1
def notified_users
-
1003
notified = []
-
# Author and assignee are always notified unless they have been
-
# locked or don't want to be notified
-
1003
notified << author if author
-
1003
if assigned_to
-
notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
-
end
-
1003
if assigned_to_was
-
notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
-
end
-
2006
notified = notified.select {|u| u.active? && u.notify_about?(self)}
-
-
1003
notified += project.notified_users
-
1003
notified.uniq!
-
# Remove users that can not view the issue
-
2821
notified.reject! {|user| !visible?(user)}
-
1003
notified
-
end
-
-
# Returns the email addresses that should be notified
-
1
def recipients
-
862
notified_users.collect(&:mail)
-
end
-
-
# Returns the number of hours spent on this issue
-
1
def spent_hours
-
1
@spent_hours ||= time_entries.sum(:hours) || 0
-
end
-
-
# Returns the total number of hours spent on this issue and its descendants
-
#
-
# Example:
-
# spent_hours => 0.0
-
# spent_hours => 50.2
-
1
def total_spent_hours
-
@total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
-
108
:joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
-
end
-
-
1
def relations
-
12
@relations ||= IssueRelations.new(self, (relations_from + relations_to).sort)
-
end
-
-
# Preloads relations for a collection of issues
-
1
def self.load_relations(issues)
-
if issues.any?
-
relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
-
issues.each do |issue|
-
issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
-
end
-
end
-
end
-
-
# Preloads visible spent time for a collection of issues
-
1
def self.load_visible_spent_hours(issues, user=User.current)
-
if issues.any?
-
hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
-
issues.each do |issue|
-
issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
-
end
-
end
-
end
-
-
# Preloads visible relations for a collection of issues
-
1
def self.load_visible_relations(issues, user=User.current)
-
if issues.any?
-
issue_ids = issues.map(&:id)
-
# Relations with issue_from in given issues and visible issue_to
-
relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
-
# Relations with issue_to in given issues and visible issue_from
-
relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
-
-
issues.each do |issue|
-
relations =
-
relations_from.select {|relation| relation.issue_from_id == issue.id} +
-
relations_to.select {|relation| relation.issue_to_id == issue.id}
-
-
issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort)
-
end
-
end
-
end
-
-
# Finds an issue relation given its id.
-
1
def find_relation(relation_id)
-
IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
-
end
-
-
1
def all_dependent_issues(except=[])
-
89
except << self
-
89
dependencies = []
-
89
relations_from.each do |relation|
-
if relation.issue_to && !except.include?(relation.issue_to)
-
dependencies << relation.issue_to
-
dependencies += relation.issue_to.all_dependent_issues(except)
-
end
-
end
-
89
dependencies
-
end
-
-
# Returns an array of issues that duplicate this one
-
1
def duplicates
-
42
relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
-
end
-
-
# Returns the due date or the target due date if any
-
# Used on gantt chart
-
1
def due_before
-
due_date || (fixed_version ? fixed_version.effective_date : nil)
-
end
-
-
# Returns the time scheduled for this issue.
-
#
-
# Example:
-
# Start Date: 2/26/09, End Date: 3/04/09
-
# duration => 6
-
1
def duration
-
(start_date && due_date) ? due_date - start_date : 0
-
end
-
-
# Returns the duration in working days
-
1
def working_duration
-
(start_date && due_date) ? working_days(start_date, due_date) : 0
-
end
-
-
1
def soonest_start(reload=false)
-
233
@soonest_start = nil if reload
-
@soonest_start ||= (
-
49
relations_to(reload).collect{|relation| relation.successor_soonest_start} +
-
233
ancestors.collect(&:soonest_start)
-
233
).compact.max
-
end
-
-
# Sets start_date on the given date or the next working day
-
# and changes due_date to keep the same working duration.
-
1
def reschedule_on(date)
-
wd = working_duration
-
date = next_working_date(date)
-
self.start_date = date
-
self.due_date = add_working_days(date, wd)
-
end
-
-
# Reschedules the issue on the given date or the next working day and saves the record.
-
# If the issue is a parent task, this is done by rescheduling its subtasks.
-
1
def reschedule_on!(date)
-
return if date.nil?
-
if leaf?
-
if start_date.nil? || start_date != date
-
if start_date && start_date > date
-
# Issue can not be moved earlier than its soonest start date
-
date = [soonest_start(true), date].compact.max
-
end
-
reschedule_on(date)
-
begin
-
save
-
rescue ActiveRecord::StaleObjectError
-
reload
-
reschedule_on(date)
-
save
-
end
-
end
-
else
-
leaves.each do |leaf|
-
if leaf.start_date
-
# Only move subtask if it starts at the same date as the parent
-
# or if it starts before the given date
-
if start_date == leaf.start_date || date > leaf.start_date
-
leaf.reschedule_on!(date)
-
end
-
else
-
leaf.reschedule_on!(date)
-
end
-
end
-
end
-
end
-
-
1
def <=>(issue)
-
if issue.nil?
-
-1
-
elsif root_id != issue.root_id
-
(root_id || 0) <=> (issue.root_id || 0)
-
else
-
(lft || 0) <=> (issue.lft || 0)
-
end
-
end
-
-
1
def to_s
-
"#{tracker} ##{id}: #{subject}"
-
end
-
-
# Returns a string of css classes that apply to the issue
-
1
def css_classes
-
726
s = "issue status-#{status_id} #{priority.try(:css_classes)}"
-
726
s << ' closed' if closed?
-
726
s << ' overdue' if overdue?
-
726
s << ' child' if child?
-
726
s << ' parent' unless leaf?
-
726
s << ' private' if is_private?
-
726
s << ' created-by-me' if User.current.logged? && author_id == User.current.id
-
726
s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
-
726
s
-
end
-
-
# Saves an issue and a time_entry from the parameters
-
1
def save_issue_with_child_records(params, existing_time_entry=nil)
-
Issue.transaction do
-
if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
-
@time_entry = existing_time_entry || TimeEntry.new
-
@time_entry.project = project
-
@time_entry.issue = self
-
@time_entry.user = User.current
-
@time_entry.spent_on = User.current.today
-
@time_entry.attributes = params[:time_entry]
-
self.time_entries << @time_entry
-
end
-
-
# TODO: Rename hook
-
Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
-
if save
-
# TODO: Rename hook
-
Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
-
else
-
raise ActiveRecord::Rollback
-
end
-
end
-
end
-
-
# Unassigns issues from +version+ if it's no longer shared with issue's project
-
1
def self.update_versions_from_sharing_change(version)
-
# Update issues assigned to the version
-
update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
-
end
-
-
# Unassigns issues from versions that are no longer shared
-
# after +project+ was moved
-
1
def self.update_versions_from_hierarchy_change(project)
-
20
moved_project_ids = project.self_and_descendants.reload.collect(&:id)
-
# Update issues of the moved projects and issues assigned to a version of a moved project
-
20
Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
-
end
-
-
1
def parent_issue_id=(arg)
-
238
s = arg.to_s.strip.presence
-
238
if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
-
236
@parent_issue.id
-
else
-
2
@parent_issue = nil
-
2
@invalid_parent_issue_id = arg
-
end
-
end
-
-
1
def parent_issue_id
-
1624
if @invalid_parent_issue_id
-
2
@invalid_parent_issue_id
-
1622
elsif instance_variable_defined? :@parent_issue
-
236
@parent_issue.nil? ? nil : @parent_issue.id
-
else
-
1386
parent_id
-
end
-
end
-
-
# Returns true if issue's project is a valid
-
# parent issue project
-
1
def valid_parent_project?(issue=parent)
-
236
return true if issue.nil? || issue.project_id == project_id
-
-
case Setting.cross_project_subtasks
-
when 'system'
-
true
-
when 'tree'
-
issue.project.root == project.root
-
when 'hierarchy'
-
issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
-
when 'descendants'
-
issue.project.is_or_is_ancestor_of?(project)
-
else
-
false
-
end
-
end
-
-
# Extracted from the ReportsController.
-
1
def self.by_tracker(project)
-
count_and_group_by(:project => project,
-
:field => 'tracker_id',
-
:joins => Tracker.table_name)
-
end
-
-
1
def self.by_version(project)
-
count_and_group_by(:project => project,
-
:field => 'fixed_version_id',
-
:joins => Version.table_name)
-
end
-
-
1
def self.by_priority(project)
-
count_and_group_by(:project => project,
-
:field => 'priority_id',
-
:joins => IssuePriority.table_name)
-
end
-
-
1
def self.by_category(project)
-
count_and_group_by(:project => project,
-
:field => 'category_id',
-
:joins => IssueCategory.table_name)
-
end
-
-
1
def self.by_assigned_to(project)
-
count_and_group_by(:project => project,
-
:field => 'assigned_to_id',
-
:joins => User.table_name)
-
end
-
-
1
def self.by_author(project)
-
count_and_group_by(:project => project,
-
:field => 'author_id',
-
:joins => User.table_name)
-
end
-
-
1
def self.by_subproject(project)
-
ActiveRecord::Base.connection.select_all("select s.id as status_id,
-
s.is_closed as closed,
-
#{Issue.table_name}.project_id as project_id,
-
count(#{Issue.table_name}.id) as total
-
from
-
#{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
-
where
-
#{Issue.table_name}.status_id=s.id
-
and #{Issue.table_name}.project_id = #{Project.table_name}.id
-
and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
-
and #{Issue.table_name}.project_id <> #{project.id}
-
group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
-
end
-
# End ReportsController extraction
-
-
# Returns an array of projects that user can assign the issue to
-
1
def allowed_target_projects(user=User.current)
-
7
if new_record?
-
4
Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
-
else
-
3
self.class.allowed_target_projects_on_move(user)
-
end
-
end
-
-
# Returns an array of projects that user can move issues to
-
1
def self.allowed_target_projects_on_move(user=User.current)
-
6
Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
-
end
-
-
1
private
-
-
1
def after_project_change
-
# Update project_id on related time entries
-
TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
-
-
# Delete issue relations
-
unless Setting.cross_project_issue_relations?
-
relations_from.clear
-
relations_to.clear
-
end
-
-
# Move subtasks that were in the same project
-
children.each do |child|
-
next unless child.project_id == project_id_was
-
# Change project and keep project
-
child.send :project=, project, true
-
unless child.save
-
raise ActiveRecord::Rollback
-
end
-
end
-
end
-
-
# Callback for after the creation of an issue by copy
-
# * adds a "copied to" relation with the copied issue
-
# * copies subtasks from the copied issue
-
1
def after_create_from_copy
-
1581
return unless copy? && !@after_create_from_copy_handled
-
-
6
if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
-
6
relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
-
6
unless relation.save
-
logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
-
end
-
end
-
-
6
unless @copied_from.leaf? || @copy_options[:subtasks] == false
-
1
@copied_from.children.each do |child|
-
2
unless child.visible?
-
# Do not copy subtasks that are not visible to avoid potential disclosure of private data
-
logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
-
next
-
end
-
2
copy = Issue.new.copy_from(child, @copy_options)
-
2
copy.author = author
-
2
copy.project = project
-
2
copy.parent_issue_id = id
-
# Children subtasks are copied recursively
-
2
unless copy.save
-
logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
-
end
-
end
-
end
-
6
@after_create_from_copy_handled = true
-
end
-
-
1
def update_nested_set_attributes
-
1581
if root_id.nil?
-
# issue was just created
-
862
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
-
862
set_default_left_and_right
-
862
Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
-
862
if @parent_issue
-
118
move_to_child_of(@parent_issue)
-
end
-
862
reload
-
elsif parent_issue_id != parent_id
-
9
former_parent_id = parent_id
-
# moving an existing issue
-
9
if @parent_issue && @parent_issue.root_id == root_id
-
# inside the same tree
-
move_to_child_of(@parent_issue)
-
else
-
# to another tree
-
9
unless root?
-
1
move_to_right_of(root)
-
1
reload
-
end
-
9
old_root_id = root_id
-
9
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
-
9
target_maxright = nested_set_scope.maximum(right_column_name) || 0
-
9
offset = target_maxright + 1 - lft
-
9
Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
-
["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
-
9
self[left_column_name] = lft + offset
-
9
self[right_column_name] = rgt + offset
-
9
if @parent_issue
-
9
move_to_child_of(@parent_issue)
-
end
-
end
-
9
reload
-
# delete invalid relations of all descendants
-
9
self_and_descendants.each do |issue|
-
9
issue.relations.each do |relation|
-
relation.destroy unless relation.valid?
-
end
-
end
-
# update former parent
-
9
recalculate_attributes_for(former_parent_id) if former_parent_id
-
end
-
1581
remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
-
end
-
-
1
def update_parent_attributes
-
2744
recalculate_attributes_for(parent_id) if parent_id
-
end
-
-
1
def recalculate_attributes_for(issue_id)
-
392
if issue_id && p = Issue.find_by_id(issue_id)
-
# priority = highest priority of children
-
392
if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
-
391
p.priority = IssuePriority.find_by_position(priority_position)
-
end
-
-
# start/due dates = lowest/highest dates of children
-
392
p.start_date = p.children.minimum(:start_date)
-
392
p.due_date = p.children.maximum(:due_date)
-
392
if p.start_date && p.due_date && p.due_date < p.start_date
-
p.start_date, p.due_date = p.due_date, p.start_date
-
end
-
-
# done ratio = weighted average ratio of leaves
-
392
unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
-
378
leaves_count = p.leaves.count
-
378
if leaves_count > 0
-
377
average = p.leaves.average(:estimated_hours).to_f
-
377
if average == 0
-
94
average = 1
-
end
-
377
done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
-
377
progress = done / (average * leaves_count)
-
377
p.done_ratio = progress.round
-
end
-
end
-
-
# estimate = sum of leaves estimates
-
392
p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
-
392
p.estimated_hours = nil if p.estimated_hours == 0.0
-
-
# ancestors will be recursively updated
-
392
p.save(:validate => false)
-
end
-
end
-
-
# Update issues so their versions are not pointing to a
-
# fixed_version that is not shared with the issue's project
-
1
def self.update_versions(conditions=nil)
-
# Only need to update issues with a fixed_version from
-
# a different project and that is not systemwide shared
-
20
Issue.scoped(:conditions => conditions).all(
-
:conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
-
" AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
-
" AND #{Version.table_name}.sharing <> 'system'",
-
:include => [:project, :fixed_version]
-
).each do |issue|
-
next if issue.project.nil? || issue.fixed_version.nil?
-
unless issue.project.shared_versions.include?(issue.fixed_version)
-
issue.init_journal(User.current)
-
issue.fixed_version = nil
-
issue.save
-
end
-
end
-
end
-
-
# Callback on file attachment
-
1
def attachment_added(obj)
-
if @current_journal && !obj.new_record?
-
@current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
-
end
-
end
-
-
# Callback on attachment deletion
-
1
def attachment_removed(obj)
-
1009
if @current_journal && !obj.new_record?
-
@current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
-
@current_journal.save
-
end
-
end
-
-
# Default assignment based on category
-
1
def default_assign
-
862
if assigned_to.nil? && category && category.assigned_to
-
self.assigned_to = category.assigned_to
-
end
-
end
-
-
# Updates start/due dates of following issues
-
1
def reschedule_following_issues
-
1581
if start_date_changed? || due_date_changed?
-
74
relations_from.each do |relation|
-
relation.set_issue_to_dates
-
end
-
end
-
end
-
-
# Closes duplicates if the issue is being closed
-
1
def close_duplicates
-
1581
if closing?
-
41
duplicates.each do |duplicate|
-
# Reload is need in case the duplicate was updated by a previous duplicate
-
duplicate.reload
-
# Don't re-close it if it's already closed
-
next if duplicate.closed?
-
# Same user and notes
-
if @current_journal
-
duplicate.init_journal(@current_journal.user, @current_journal.notes)
-
end
-
duplicate.update_attribute :status, self.status
-
end
-
end
-
end
-
-
# Make sure updated_on is updated when adding a note
-
1
def force_updated_on_change
-
1581
if @current_journal
-
203
self.updated_on = current_time_from_proper_timezone
-
end
-
end
-
-
# Saves the changes in a Journal
-
# Called after_save
-
1
def create_journal
-
1581
if @current_journal
-
# attributes changes
-
203
if @attributes_before_change
-
203
(Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
-
4060
before = @attributes_before_change[c]
-
4060
after = send(c)
-
4060
next if before == after || (before.blank? && after.blank?)
-
@current_journal.details << JournalDetail.new(:property => 'attr',
-
:prop_key => c,
-
:old_value => before,
-
232
:value => after)
-
}
-
end
-
203
if @custom_values_before_change
-
# custom fields changes
-
203
custom_field_values.each {|c|
-
before = @custom_values_before_change[c.custom_field_id]
-
after = c.value
-
next if before == after || (before.blank? && after.blank?)
-
-
if before.is_a?(Array) || after.is_a?(Array)
-
before = [before] unless before.is_a?(Array)
-
after = [after] unless after.is_a?(Array)
-
-
# values removed
-
(before - after).reject(&:blank?).each do |value|
-
@current_journal.details << JournalDetail.new(:property => 'cf',
-
:prop_key => c.custom_field_id,
-
:old_value => value,
-
:value => nil)
-
end
-
# values added
-
(after - before).reject(&:blank?).each do |value|
-
@current_journal.details << JournalDetail.new(:property => 'cf',
-
:prop_key => c.custom_field_id,
-
:old_value => nil,
-
:value => value)
-
end
-
else
-
@current_journal.details << JournalDetail.new(:property => 'cf',
-
:prop_key => c.custom_field_id,
-
:old_value => before,
-
:value => after)
-
end
-
}
-
end
-
203
@current_journal.save
-
# reset current journal
-
203
init_journal @current_journal.user, @current_journal.notes
-
end
-
end
-
-
# Query generator for selecting groups of issue counts for a project
-
# based on specific criteria
-
#
-
# Options
-
# * project - Project to search in.
-
# * field - String. Issue field to key off of in the grouping.
-
# * joins - String. The table name to join against.
-
1
def self.count_and_group_by(options)
-
project = options.delete(:project)
-
select_field = options.delete(:field)
-
joins = options.delete(:joins)
-
-
where = "#{Issue.table_name}.#{select_field}=j.id"
-
-
ActiveRecord::Base.connection.select_all("select s.id as status_id,
-
s.is_closed as closed,
-
j.id as #{select_field},
-
count(#{Issue.table_name}.id) as total
-
from
-
#{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
-
where
-
#{Issue.table_name}.status_id=s.id
-
and #{where}
-
and #{Issue.table_name}.project_id=#{Project.table_name}.id
-
and #{visible_condition(User.current, :project => project)}
-
group by s.id, s.is_closed, j.id")
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueCategory < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :project
-
1
belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
-
1
has_many :issues, :foreign_key => 'category_id', :dependent => :nullify
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name, :scope => [:project_id]
-
1
validates_length_of :name, :maximum => 30
-
-
1
safe_attributes 'name', 'assigned_to_id'
-
-
1
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
-
-
1
alias :destroy_without_reassign :destroy
-
-
# Destroy the category
-
# If a category is specified, issues are reassigned to this category
-
1
def destroy(reassign_to = nil)
-
if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project
-
Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}")
-
end
-
destroy_without_reassign
-
end
-
-
1
def <=>(category)
-
name <=> category.name
-
end
-
-
1
def to_s; name end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueCustomField < CustomField
-
1
has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id"
-
1
has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id"
-
1
has_many :issues, :through => :issue_custom_values
-
-
1
def type_name
-
:label_issue_plural
-
end
-
end
-
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueObserver < ActiveRecord::Observer
-
1
def after_create(issue)
-
862
Mailer.issue_add(issue).deliver if Setting.notified_events.include?('issue_added')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssuePriority < Enumeration
-
1
has_many :issues, :foreign_key => 'priority_id'
-
-
1
after_destroy {|priority| priority.class.compute_position_names}
-
1
after_save {|priority| priority.class.compute_position_names if priority.position_changed? && priority.position}
-
-
1
OptionName = :enumeration_issue_priorities
-
-
1
def option_name
-
OptionName
-
end
-
-
1
def objects_count
-
issues.count
-
end
-
-
1
def transfer_relations(to)
-
issues.update_all("priority_id = #{to.id}")
-
end
-
-
1
def css_classes
-
726
"priority-#{id} priority-#{position_name}"
-
end
-
-
# Clears position_name for all priorities
-
# Called from migration 20121026003537_populate_enumerations_position_name
-
1
def self.clear_position_names
-
update_all :position_name => nil
-
end
-
-
# Updates position_name for active priorities
-
# Called from migration 20121026003537_populate_enumerations_position_name
-
1
def self.compute_position_names
-
priorities = where(:active => true).all.sort_by(&:position)
-
if priorities.any?
-
default = priorities.detect(&:is_default?) || priorities[(priorities.size - 1) / 2]
-
priorities.each_with_index do |priority, index|
-
name = case
-
when priority.position == default.position
-
"default"
-
when priority.position < default.position
-
index == 0 ? "lowest" : "low#{index+1}"
-
else
-
index == (priorities.size - 1) ? "highest" : "high#{priorities.size - index}"
-
end
-
-
update_all({:position_name => name}, :id => priority.id)
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssuePriorityCustomField < CustomField
-
1
def type_name
-
:enumeration_issue_priorities
-
end
-
end
-
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
# Class used to represent the relations of an issue
-
1
class IssueRelations < Array
-
1
include Redmine::I18n
-
-
1
def initialize(issue, *args)
-
12
@issue = issue
-
12
super(*args)
-
end
-
-
1
def to_s(*args)
-
map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ')
-
end
-
end
-
-
1
class IssueRelation < ActiveRecord::Base
-
1
belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
-
1
belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
-
-
1
TYPE_RELATES = "relates"
-
1
TYPE_DUPLICATES = "duplicates"
-
1
TYPE_DUPLICATED = "duplicated"
-
1
TYPE_BLOCKS = "blocks"
-
1
TYPE_BLOCKED = "blocked"
-
1
TYPE_PRECEDES = "precedes"
-
1
TYPE_FOLLOWS = "follows"
-
1
TYPE_COPIED_TO = "copied_to"
-
1
TYPE_COPIED_FROM = "copied_from"
-
-
1
TYPES = {
-
TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to,
-
:order => 1, :sym => TYPE_RELATES },
-
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by,
-
:order => 2, :sym => TYPE_DUPLICATED },
-
TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates,
-
:order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
-
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by,
-
:order => 4, :sym => TYPE_BLOCKED },
-
TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks,
-
:order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
-
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows,
-
:order => 6, :sym => TYPE_FOLLOWS },
-
TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes,
-
:order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES },
-
TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from,
-
:order => 8, :sym => TYPE_COPIED_FROM },
-
TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to,
-
:order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO }
-
}.freeze
-
-
1
validates_presence_of :issue_from, :issue_to, :relation_type
-
1
validates_inclusion_of :relation_type, :in => TYPES.keys
-
1
validates_numericality_of :delay, :allow_nil => true
-
1
validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
-
1
validate :validate_issue_relation
-
-
1
attr_protected :issue_from_id, :issue_to_id
-
1
before_save :handle_issue_order
-
-
1
def visible?(user=User.current)
-
(issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
-
end
-
-
1
def deletable?(user=User.current)
-
visible?(user) &&
-
((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) ||
-
(issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
-
end
-
-
1
def initialize(attributes=nil, *args)
-
89
super
-
89
if new_record?
-
89
if relation_type.blank?
-
2
self.relation_type = IssueRelation::TYPE_RELATES
-
end
-
end
-
end
-
-
1
def validate_issue_relation
-
89
if issue_from && issue_to
-
89
errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
-
unless issue_from.project_id == issue_to.project_id ||
-
89
Setting.cross_project_issue_relations?
-
errors.add :issue_to_id, :not_same_project
-
end
-
# detect circular dependencies depending wether the relation should be reversed
-
89
if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
-
errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to
-
else
-
89
errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from
-
end
-
89
if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
-
errors.add :base, :cant_link_an_issue_with_a_descendant
-
end
-
end
-
end
-
-
1
def other_issue(issue)
-
14
(self.issue_from_id == issue.id) ? issue_to : issue_from
-
end
-
-
# Returns the relation type for +issue+
-
1
def relation_type_for(issue)
-
if TYPES[relation_type]
-
if self.issue_from_id == issue.id
-
relation_type
-
else
-
TYPES[relation_type][:sym]
-
end
-
end
-
end
-
-
1
def label_for(issue)
-
2
TYPES[relation_type] ?
-
2
TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] :
-
:unknow
-
end
-
-
1
def css_classes_for(issue)
-
"rel-#{relation_type_for(issue)}"
-
end
-
-
1
def handle_issue_order
-
87
reverse_if_needed
-
-
87
if TYPE_PRECEDES == relation_type
-
self.delay ||= 0
-
else
-
87
self.delay = nil
-
end
-
87
set_issue_to_dates
-
end
-
-
1
def set_issue_to_dates
-
87
soonest_start = self.successor_soonest_start
-
87
if soonest_start && issue_to
-
issue_to.reschedule_on!(soonest_start)
-
end
-
end
-
-
1
def successor_soonest_start
-
136
if (TYPE_PRECEDES == self.relation_type) && delay && issue_from &&
-
(issue_from.start_date || issue_from.due_date)
-
(issue_from.due_date || issue_from.start_date) + 1 + delay
-
end
-
end
-
-
1
def <=>(relation)
-
r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
-
r == 0 ? id <=> relation.id : r
-
end
-
-
1
private
-
-
# Reverses the relation if needed so that it gets stored in the proper way
-
# Should not be reversed before validation so that it can be displayed back
-
# as entered on new relation form
-
1
def reverse_if_needed
-
87
if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
-
issue_tmp = issue_to
-
self.issue_to = issue_from
-
self.issue_from = issue_tmp
-
self.relation_type = TYPES[relation_type][:reverse]
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueStatus < ActiveRecord::Base
-
1
before_destroy :check_integrity
-
1
has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id"
-
1
acts_as_list
-
-
1
before_destroy :delete_workflow_rules
-
1
after_save :update_default
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name
-
1
validates_length_of :name, :maximum => 30
-
1
validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
-
-
1
scope :sorted, order("#{table_name}.position ASC")
-
1
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
-
-
1
def update_default
-
173
IssueStatus.update_all({:is_default => false}, ['id <> ?', id]) if self.is_default?
-
end
-
-
# Returns the default status for new issues
-
1
def self.default
-
2482
where(:is_default => true).first
-
end
-
-
# Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
-
1
def self.update_issue_done_ratios
-
if Issue.use_status_for_done_ratio?
-
IssueStatus.where("default_done_ratio >= 0").all.each do |status|
-
Issue.update_all({:done_ratio => status.default_done_ratio}, {:status_id => status.id})
-
end
-
end
-
-
return Issue.use_status_for_done_ratio?
-
end
-
-
# Returns an array of all statuses the given role can switch to
-
# Uses association cache when called more than one time
-
1
def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
-
1560
if roles && tracker
-
1560
role_ids = roles.collect(&:id)
-
1560
transitions = workflows.select do |w|
-
role_ids.include?(w.role_id) &&
-
116480
w.tracker_id == tracker.id &&
-
6120
((!w.author && !w.assignee) || (author && w.author) || (assignee && w.assignee))
-
end
-
1560
transitions.map(&:new_status).compact.sort
-
else
-
[]
-
end
-
end
-
-
# Same thing as above but uses a database query
-
# More efficient than the previous method if called just once
-
1
def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
-
251
if roles.present? && tracker
-
22
conditions = "(author = :false AND assignee = :false)"
-
22
conditions << " OR author = :true" if author
-
22
conditions << " OR assignee = :true" if assignee
-
-
22
workflows.
-
includes(:new_status).
-
where(["role_id IN (:role_ids) AND tracker_id = :tracker_id AND (#{conditions})",
-
{:role_ids => roles.collect(&:id), :tracker_id => tracker.id, :true => true, :false => false}
-
]).all.
-
map(&:new_status).compact.sort
-
else
-
229
[]
-
end
-
end
-
-
1
def <=>(status)
-
8270
position <=> status.position
-
end
-
-
2734
def to_s; name end
-
-
1
private
-
-
1
def check_integrity
-
raise "Can't delete status" if Issue.where(:status_id => id).any?
-
end
-
-
# Deletes associated workflows
-
1
def delete_workflow_rules
-
WorkflowRule.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Journal < ActiveRecord::Base
-
1
belongs_to :journalized, :polymorphic => true
-
# added as a quick fix to allow eager loading of the polymorphic association
-
# since always associated to an issue, for now
-
1
belongs_to :issue, :foreign_key => :journalized_id
-
-
1
belongs_to :user
-
1
has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
-
1
attr_accessor :indice
-
-
acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
-
:description => :notes,
-
:author => :user,
-
:type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' },
-
1
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
-
-
acts_as_activity_provider :type => 'issues',
-
:author_key => :user_id,
-
:find_options => {:include => [{:issue => :project}, :details, :user],
-
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
-
1
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
-
-
1
before_create :split_private_notes
-
-
1
scope :visible, lambda {|*args|
-
user = args.shift || User.current
-
-
includes(:issue => :project).
-
where(Issue.visible_condition(user, *args)).
-
where("(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes, *args)}))", false)
-
}
-
-
1
def save(*args)
-
# Do not save an empty journal
-
203
(details.empty? && notes.blank?) ? false : super
-
end
-
-
# Returns the new status if the journal contains a status change, otherwise nil
-
1
def new_status
-
c = details.detect {|detail| detail.prop_key == 'status_id'}
-
(c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
-
end
-
-
1
def new_value_for(prop)
-
283
c = details.detect {|detail| detail.prop_key == prop}
-
141
c ? c.value : nil
-
end
-
-
1
def editable_by?(usr)
-
usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
-
end
-
-
1
def project
-
journalized.respond_to?(:project) ? journalized.project : nil
-
end
-
-
1
def attachments
-
journalized.respond_to?(:attachments) ? journalized.attachments : nil
-
end
-
-
# Returns a string of css classes
-
1
def css_classes
-
s = 'journal'
-
s << ' has-notes' unless notes.blank?
-
s << ' has-details' unless details.blank?
-
s << ' private-notes' if private_notes?
-
s
-
end
-
-
1
def notify?
-
141
@notify != false
-
end
-
-
1
def notify=(arg)
-
@notify = arg
-
end
-
-
1
def recipients
-
141
notified = journalized.notified_users
-
141
if private_notes?
-
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
-
end
-
141
notified.map(&:mail)
-
end
-
-
1
def watcher_recipients
-
141
notified = journalized.notified_watchers
-
141
if private_notes?
-
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
-
end
-
141
notified.map(&:mail)
-
end
-
-
1
private
-
-
1
def split_private_notes
-
141
if private_notes?
-
if notes.present?
-
if details.any?
-
# Split the journal (notes/changes) so we don't have half-private journals
-
journal = Journal.new(:journalized => journalized, :user => user, :notes => nil, :private_notes => false)
-
journal.details = details
-
journal.save
-
self.details = []
-
self.created_on = journal.created_on
-
end
-
else
-
# Blank notes should not be private
-
self.private_notes = false
-
end
-
end
-
141
true
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class JournalDetail < ActiveRecord::Base
-
1
belongs_to :journal
-
1
before_save :normalize_values
-
-
1
private
-
-
1
def normalize_values
-
232
self.value = normalize(value)
-
232
self.old_value = normalize(old_value)
-
end
-
-
1
def normalize(v)
-
464
if v == true
-
"1"
-
464
elsif v == false
-
"0"
-
else
-
464
v
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class JournalObserver < ActiveRecord::Observer
-
1
def after_create(journal)
-
if journal.notify? &&
-
(Setting.notified_events.include?('issue_updated') ||
-
(Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
-
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
-
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
-
141
)
-
141
Mailer.issue_edit(journal).deliver
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MailHandler < ActionMailer::Base
-
1
include ActionView::Helpers::SanitizeHelper
-
1
include Redmine::I18n
-
-
1
class UnauthorizedAction < StandardError; end
-
1
class MissingInformation < StandardError; end
-
-
1
attr_reader :email, :user
-
-
1
def self.receive(email, options={})
-
@@handler_options = options.dup
-
-
@@handler_options[:issue] ||= {}
-
-
if @@handler_options[:allow_override].is_a?(String)
-
@@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
-
end
-
@@handler_options[:allow_override] ||= []
-
# Project needs to be overridable if not specified
-
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
-
# Status overridable by default
-
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
-
-
@@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
-
-
email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
-
super(email)
-
end
-
-
1
def logger
-
Rails.logger
-
end
-
-
1
cattr_accessor :ignored_emails_headers
-
1
@@ignored_emails_headers = {
-
'X-Auto-Response-Suppress' => 'oof',
-
'Auto-Submitted' => /^auto-/
-
}
-
-
# Processes incoming emails
-
# Returns the created object (eg. an issue, a message) or false
-
1
def receive(email)
-
@email = email
-
sender_email = email.from.to_a.first.to_s.strip
-
# Ignore emails received from the application emission address to avoid hell cycles
-
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
-
if logger && logger.info
-
logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
-
end
-
return false
-
end
-
# Ignore auto generated emails
-
self.class.ignored_emails_headers.each do |key, ignored_value|
-
value = email.header[key]
-
if value
-
value = value.to_s.downcase
-
if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
-
if logger && logger.info
-
logger.info "MailHandler: ignoring email with #{key}:#{value} header"
-
end
-
return false
-
end
-
end
-
end
-
@user = User.find_by_mail(sender_email) if sender_email.present?
-
if @user && !@user.active?
-
if logger && logger.info
-
logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
-
end
-
return false
-
end
-
if @user.nil?
-
# Email was submitted by an unknown user
-
case @@handler_options[:unknown_user]
-
when 'accept'
-
@user = User.anonymous
-
when 'create'
-
@user = create_user_from_email
-
if @user
-
if logger && logger.info
-
logger.info "MailHandler: [#{@user.login}] account created"
-
end
-
Mailer.account_information(@user, @user.password).deliver
-
else
-
if logger && logger.error
-
logger.error "MailHandler: could not create account for [#{sender_email}]"
-
end
-
return false
-
end
-
else
-
# Default behaviour, emails from unknown users are ignored
-
if logger && logger.info
-
logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
-
end
-
return false
-
end
-
end
-
User.current = @user
-
dispatch
-
end
-
-
1
private
-
-
1
MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
-
1
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
-
1
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
-
-
1
def dispatch
-
headers = [email.in_reply_to, email.references].flatten.compact
-
subject = email.subject.to_s
-
if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
-
klass, object_id = $1, $2.to_i
-
method_name = "receive_#{klass}_reply"
-
if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
-
send method_name, object_id
-
else
-
# ignoring it
-
end
-
elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
-
receive_issue_reply(m[1].to_i)
-
elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
-
receive_message_reply(m[1].to_i)
-
else
-
dispatch_to_default
-
end
-
rescue ActiveRecord::RecordInvalid => e
-
# TODO: send a email to the user
-
logger.error e.message if logger
-
false
-
rescue MissingInformation => e
-
logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
-
false
-
rescue UnauthorizedAction => e
-
logger.error "MailHandler: unauthorized attempt from #{user}" if logger
-
false
-
end
-
-
1
def dispatch_to_default
-
receive_issue
-
end
-
-
# Creates a new issue
-
1
def receive_issue
-
project = target_project
-
# check permission
-
unless @@handler_options[:no_permission_check]
-
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
-
end
-
-
issue = Issue.new(:author => user, :project => project)
-
issue.safe_attributes = issue_attributes_from_keywords(issue)
-
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
-
issue.subject = cleaned_up_subject
-
if issue.subject.blank?
-
issue.subject = '(no subject)'
-
end
-
issue.description = cleaned_up_text_body
-
-
# add To and Cc as watchers before saving so the watchers can reply to Redmine
-
add_watchers(issue)
-
issue.save!
-
add_attachments(issue)
-
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
-
issue
-
end
-
-
# Adds a note to an existing issue
-
1
def receive_issue_reply(issue_id, from_journal=nil)
-
issue = Issue.find_by_id(issue_id)
-
return unless issue
-
# check permission
-
unless @@handler_options[:no_permission_check]
-
unless user.allowed_to?(:add_issue_notes, issue.project) ||
-
user.allowed_to?(:edit_issues, issue.project)
-
raise UnauthorizedAction
-
end
-
end
-
-
# ignore CLI-supplied defaults for new issues
-
@@handler_options[:issue].clear
-
-
journal = issue.init_journal(user)
-
if from_journal && from_journal.private_notes?
-
# If the received email was a reply to a private note, make the added note private
-
issue.private_notes = true
-
end
-
issue.safe_attributes = issue_attributes_from_keywords(issue)
-
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
-
journal.notes = cleaned_up_text_body
-
add_attachments(issue)
-
issue.save!
-
if logger && logger.info
-
logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
-
end
-
journal
-
end
-
-
# Reply will be added to the issue
-
1
def receive_journal_reply(journal_id)
-
journal = Journal.find_by_id(journal_id)
-
if journal && journal.journalized_type == 'Issue'
-
receive_issue_reply(journal.journalized_id, journal)
-
end
-
end
-
-
# Receives a reply to a forum message
-
1
def receive_message_reply(message_id)
-
message = Message.find_by_id(message_id)
-
if message
-
message = message.root
-
-
unless @@handler_options[:no_permission_check]
-
raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
-
end
-
-
if !message.locked?
-
reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
-
:content => cleaned_up_text_body)
-
reply.author = user
-
reply.board = message.board
-
message.children << reply
-
add_attachments(reply)
-
reply
-
else
-
if logger && logger.info
-
logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
-
end
-
end
-
end
-
end
-
-
1
def add_attachments(obj)
-
if email.attachments && email.attachments.any?
-
email.attachments.each do |attachment|
-
filename = attachment.filename
-
unless filename.respond_to?(:encoding)
-
# try to reencode to utf8 manually with ruby1.8
-
h = attachment.header['Content-Disposition']
-
unless h.nil?
-
begin
-
if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/)
-
filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
-
elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/)
-
# http://tools.ietf.org/html/rfc2047#section-4
-
filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
-
end
-
rescue
-
# nop
-
end
-
end
-
end
-
obj.attachments << Attachment.create(:container => obj,
-
:file => attachment.decoded,
-
:filename => filename,
-
:author => user,
-
:content_type => attachment.mime_type)
-
end
-
end
-
end
-
-
# Adds To and Cc as watchers of the given object if the sender has the
-
# appropriate permission
-
1
def add_watchers(obj)
-
if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
-
addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
-
unless addresses.empty?
-
watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
-
watchers.each {|w| obj.add_watcher(w)}
-
end
-
end
-
end
-
-
1
def get_keyword(attr, options={})
-
@keywords ||= {}
-
if @keywords.has_key?(attr)
-
@keywords[attr]
-
else
-
@keywords[attr] = begin
-
if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
-
(v = extract_keyword!(plain_text_body, attr, options[:format]))
-
v
-
elsif !@@handler_options[:issue][attr].blank?
-
@@handler_options[:issue][attr]
-
end
-
end
-
end
-
end
-
-
# Destructively extracts the value for +attr+ in +text+
-
# Returns nil if no matching keyword found
-
1
def extract_keyword!(text, attr, format=nil)
-
keys = [attr.to_s.humanize]
-
if attr.is_a?(Symbol)
-
if user && user.language.present?
-
keys << l("field_#{attr}", :default => '', :locale => user.language)
-
end
-
if Setting.default_language.present?
-
keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
-
end
-
end
-
keys.reject! {|k| k.blank?}
-
keys.collect! {|k| Regexp.escape(k)}
-
format ||= '.+'
-
keyword = nil
-
regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
-
if m = text.match(regexp)
-
keyword = m[2].strip
-
text.gsub!(regexp, '')
-
end
-
keyword
-
end
-
-
1
def target_project
-
# TODO: other ways to specify project:
-
# * parse the email To field
-
# * specific project (eg. Setting.mail_handler_target_project)
-
target = Project.find_by_identifier(get_keyword(:project))
-
raise MissingInformation.new('Unable to determine target project') if target.nil?
-
target
-
end
-
-
# Returns a Hash of issue attributes extracted from keywords in the email body
-
1
def issue_attributes_from_keywords(issue)
-
assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
-
-
attrs = {
-
'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
-
'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
-
'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
-
'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
-
'assigned_to_id' => assigned_to.try(:id),
-
'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
-
issue.project.shared_versions.named(k).first.try(:id),
-
'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
-
'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
-
'estimated_hours' => get_keyword(:estimated_hours, :override => true),
-
'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
-
}.delete_if {|k, v| v.blank? }
-
-
if issue.new_record? && attrs['tracker_id'].nil?
-
attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
-
end
-
-
attrs
-
end
-
-
# Returns a Hash of issue custom field values extracted from keywords in the email body
-
1
def custom_field_values_from_keywords(customized)
-
customized.custom_field_values.inject({}) do |h, v|
-
if keyword = get_keyword(v.custom_field.name, :override => true)
-
h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
-
end
-
h
-
end
-
end
-
-
# Returns the text/plain part of the email
-
# If not found (eg. HTML-only email), returns the body with tags removed
-
1
def plain_text_body
-
return @plain_text_body unless @plain_text_body.nil?
-
-
part = email.text_part || email.html_part || email
-
@plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
-
-
# strip html tags and remove doctype directive
-
@plain_text_body = strip_tags(@plain_text_body.strip)
-
@plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
-
@plain_text_body
-
end
-
-
1
def cleaned_up_text_body
-
cleanup_body(plain_text_body)
-
end
-
-
1
def cleaned_up_subject
-
subject = email.subject.to_s
-
unless subject.respond_to?(:encoding)
-
# try to reencode to utf8 manually with ruby1.8
-
begin
-
if h = email.header[:subject]
-
# http://tools.ietf.org/html/rfc2047#section-4
-
if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
-
subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
-
end
-
end
-
rescue
-
# nop
-
end
-
end
-
subject.strip[0,255]
-
end
-
-
1
def self.full_sanitizer
-
@full_sanitizer ||= HTML::FullSanitizer.new
-
end
-
-
1
def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
-
limit ||= object.class.columns_hash[attribute.to_s].limit || 255
-
value = value.to_s.slice(0, limit)
-
object.send("#{attribute}=", value)
-
end
-
-
# Returns a User from an email address and a full name
-
1
def self.new_user_from_attributes(email_address, fullname=nil)
-
user = User.new
-
-
# Truncating the email address would result in an invalid format
-
user.mail = email_address
-
assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
-
-
names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
-
assign_string_attribute_with_limit(user, 'firstname', names.shift)
-
assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
-
user.lastname = '-' if user.lastname.blank?
-
-
password_length = [Setting.password_min_length.to_i, 10].max
-
user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
-
user.language = Setting.default_language
-
-
unless user.valid?
-
user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
-
user.firstname = "-" unless user.errors[:firstname].blank?
-
user.lastname = "-" unless user.errors[:lastname].blank?
-
end
-
-
user
-
end
-
-
# Creates a User for the +email+ sender
-
# Returns the user or nil if it could not be created
-
1
def create_user_from_email
-
from = email.header['from'].to_s
-
addr, name = from, nil
-
if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
-
addr, name = m[2], m[1]
-
end
-
if addr.present?
-
user = self.class.new_user_from_attributes(addr, name)
-
if user.save
-
user
-
else
-
logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
-
nil
-
end
-
else
-
logger.error "MailHandler: failed to create User: no FROM address found" if logger
-
nil
-
end
-
end
-
-
# Removes the email body of text after the truncation configurations.
-
1
def cleanup_body(body)
-
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
-
unless delimiters.empty?
-
regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
-
body = body.gsub(regex, '')
-
end
-
body.strip
-
end
-
-
1
def find_assignee_from_keyword(keyword, issue)
-
keyword = keyword.to_s.downcase
-
assignable = issue.assignable_users
-
assignee = nil
-
assignee ||= assignable.detect {|a|
-
a.mail.to_s.downcase == keyword ||
-
a.login.to_s.downcase == keyword
-
}
-
if assignee.nil? && keyword.match(/ /)
-
firstname, lastname = *(keyword.split) # "First Last Throwaway"
-
assignee ||= assignable.detect {|a|
-
a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
-
a.lastname.to_s.downcase == lastname
-
}
-
end
-
if assignee.nil?
-
assignee ||= assignable.detect {|a| a.name.downcase == keyword}
-
end
-
assignee
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Mailer < ActionMailer::Base
-
1
layout 'mailer'
-
1
helper :application
-
1
helper :issues
-
1
helper :custom_fields
-
-
1
include Redmine::I18n
-
-
1
def self.default_url_options
-
1003
{ :host => Setting.host_name, :protocol => Setting.protocol }
-
end
-
-
# Builds a Mail::Message object used to email recipients of the added issue.
-
#
-
# Example:
-
# issue_add(issue) => Mail::Message object
-
# Mailer.issue_add(issue).deliver => sends an email to issue recipients
-
1
def issue_add(issue)
-
redmine_headers 'Project' => issue.project.identifier,
-
'Issue-Id' => issue.id,
-
862
'Issue-Author' => issue.author.login
-
862
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
-
862
message_id issue
-
862
@author = issue.author
-
862
@issue = issue
-
862
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
-
862
recipients = issue.recipients
-
862
cc = issue.watcher_recipients - recipients
-
mail :to => recipients,
-
:cc => cc,
-
862
:subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
-
end
-
-
# Builds a Mail::Message object used to email recipients of the edited issue.
-
#
-
# Example:
-
# issue_edit(journal) => Mail::Message object
-
# Mailer.issue_edit(journal).deliver => sends an email to issue recipients
-
1
def issue_edit(journal)
-
141
issue = journal.journalized.reload
-
redmine_headers 'Project' => issue.project.identifier,
-
'Issue-Id' => issue.id,
-
141
'Issue-Author' => issue.author.login
-
141
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
-
141
message_id journal
-
141
references issue
-
141
@author = journal.user
-
141
recipients = journal.recipients
-
# Watchers in cc
-
141
cc = journal.watcher_recipients - recipients
-
141
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
-
141
s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
-
141
s << issue.subject
-
141
@issue = issue
-
141
@journal = journal
-
141
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
-
mail :to => recipients,
-
:cc => cc,
-
141
:subject => s
-
end
-
-
1
def reminder(user, issues, days)
-
set_language_if_valid user.language
-
@issues = issues
-
@days = days
-
@issues_url = url_for(:controller => 'issues', :action => 'index',
-
:set_filter => 1, :assigned_to_id => user.id,
-
:sort => 'due_date:asc')
-
mail :to => user.mail,
-
:subject => l(:mail_subject_reminder, :count => issues.size, :days => days)
-
end
-
-
# Builds a Mail::Message object used to email users belonging to the added document's project.
-
#
-
# Example:
-
# document_added(document) => Mail::Message object
-
# Mailer.document_added(document).deliver => sends an email to the document's project recipients
-
1
def document_added(document)
-
redmine_headers 'Project' => document.project.identifier
-
@author = User.current
-
@document = document
-
@document_url = url_for(:controller => 'documents', :action => 'show', :id => document)
-
mail :to => document.recipients,
-
:subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
-
end
-
-
# Builds a Mail::Message object used to email recipients of a project when an attachements are added.
-
#
-
# Example:
-
# attachments_added(attachments) => Mail::Message object
-
# Mailer.attachments_added(attachments).deliver => sends an email to the project's recipients
-
1
def attachments_added(attachments)
-
container = attachments.first.container
-
added_to = ''
-
added_to_url = ''
-
@author = attachments.first.author
-
case container.class.name
-
when 'Project'
-
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
-
added_to = "#{l(:label_project)}: #{container}"
-
recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
-
when 'Version'
-
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
-
added_to = "#{l(:label_version)}: #{container.name}"
-
recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
-
when 'Document'
-
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
-
added_to = "#{l(:label_document)}: #{container.title}"
-
recipients = container.recipients
-
end
-
redmine_headers 'Project' => container.project.identifier
-
@attachments = attachments
-
@added_to = added_to
-
@added_to_url = added_to_url
-
mail :to => recipients,
-
:subject => "[#{container.project.name}] #{l(:label_attachment_new)}"
-
end
-
-
# Builds a Mail::Message object used to email recipients of a news' project when a news item is added.
-
#
-
# Example:
-
# news_added(news) => Mail::Message object
-
# Mailer.news_added(news).deliver => sends an email to the news' project recipients
-
1
def news_added(news)
-
redmine_headers 'Project' => news.project.identifier
-
@author = news.author
-
message_id news
-
@news = news
-
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
-
mail :to => news.recipients,
-
:subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
-
end
-
-
# Builds a Mail::Message object used to email recipients of a news' project when a news comment is added.
-
#
-
# Example:
-
# news_comment_added(comment) => Mail::Message object
-
# Mailer.news_comment_added(comment) => sends an email to the news' project recipients
-
1
def news_comment_added(comment)
-
news = comment.commented
-
redmine_headers 'Project' => news.project.identifier
-
@author = comment.author
-
message_id comment
-
@news = news
-
@comment = comment
-
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
-
mail :to => news.recipients,
-
:cc => news.watcher_recipients,
-
:subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
-
end
-
-
# Builds a Mail::Message object used to email the recipients of the specified message that was posted.
-
#
-
# Example:
-
# message_posted(message) => Mail::Message object
-
# Mailer.message_posted(message).deliver => sends an email to the recipients
-
1
def message_posted(message)
-
redmine_headers 'Project' => message.project.identifier,
-
'Topic-Id' => (message.parent_id || message.id)
-
@author = message.author
-
message_id message
-
references message.parent unless message.parent.nil?
-
recipients = message.recipients
-
cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
-
@message = message
-
@message_url = url_for(message.event_url)
-
mail :to => recipients,
-
:cc => cc,
-
:subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
-
end
-
-
# Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was added.
-
#
-
# Example:
-
# wiki_content_added(wiki_content) => Mail::Message object
-
# Mailer.wiki_content_added(wiki_content).deliver => sends an email to the project's recipients
-
1
def wiki_content_added(wiki_content)
-
redmine_headers 'Project' => wiki_content.project.identifier,
-
'Wiki-Page-Id' => wiki_content.page.id
-
@author = wiki_content.author
-
message_id wiki_content
-
recipients = wiki_content.recipients
-
cc = wiki_content.page.wiki.watcher_recipients - recipients
-
@wiki_content = wiki_content
-
@wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
-
:project_id => wiki_content.project,
-
:id => wiki_content.page.title)
-
mail :to => recipients,
-
:cc => cc,
-
:subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
-
end
-
-
# Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was updated.
-
#
-
# Example:
-
# wiki_content_updated(wiki_content) => Mail::Message object
-
# Mailer.wiki_content_updated(wiki_content).deliver => sends an email to the project's recipients
-
1
def wiki_content_updated(wiki_content)
-
redmine_headers 'Project' => wiki_content.project.identifier,
-
'Wiki-Page-Id' => wiki_content.page.id
-
@author = wiki_content.author
-
message_id wiki_content
-
recipients = wiki_content.recipients
-
cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients
-
@wiki_content = wiki_content
-
@wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
-
:project_id => wiki_content.project,
-
:id => wiki_content.page.title)
-
@wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff',
-
:project_id => wiki_content.project, :id => wiki_content.page.title,
-
:version => wiki_content.version)
-
mail :to => recipients,
-
:cc => cc,
-
:subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
-
end
-
-
# Builds a Mail::Message object used to email the specified user their account information.
-
#
-
# Example:
-
# account_information(user, password) => Mail::Message object
-
# Mailer.account_information(user, password).deliver => sends account information to the user
-
1
def account_information(user, password)
-
set_language_if_valid user.language
-
@user = user
-
@password = password
-
@login_url = url_for(:controller => 'account', :action => 'login')
-
mail :to => user.mail,
-
:subject => l(:mail_subject_register, Setting.app_title)
-
end
-
-
# Builds a Mail::Message object used to email all active administrators of an account activation request.
-
#
-
# Example:
-
# account_activation_request(user) => Mail::Message object
-
# Mailer.account_activation_request(user).deliver => sends an email to all active administrators
-
1
def account_activation_request(user)
-
# Send the email to all active administrators
-
recipients = User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
-
@user = user
-
@url = url_for(:controller => 'users', :action => 'index',
-
:status => User::STATUS_REGISTERED,
-
:sort_key => 'created_on', :sort_order => 'desc')
-
mail :to => recipients,
-
:subject => l(:mail_subject_account_activation_request, Setting.app_title)
-
end
-
-
# Builds a Mail::Message object used to email the specified user that their account was activated by an administrator.
-
#
-
# Example:
-
# account_activated(user) => Mail::Message object
-
# Mailer.account_activated(user).deliver => sends an email to the registered user
-
1
def account_activated(user)
-
set_language_if_valid user.language
-
@user = user
-
@login_url = url_for(:controller => 'account', :action => 'login')
-
mail :to => user.mail,
-
:subject => l(:mail_subject_register, Setting.app_title)
-
end
-
-
1
def lost_password(token)
-
set_language_if_valid(token.user.language)
-
@token = token
-
@url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
-
mail :to => token.user.mail,
-
:subject => l(:mail_subject_lost_password, Setting.app_title)
-
end
-
-
1
def register(token)
-
set_language_if_valid(token.user.language)
-
@token = token
-
@url = url_for(:controller => 'account', :action => 'activate', :token => token.value)
-
mail :to => token.user.mail,
-
:subject => l(:mail_subject_register, Setting.app_title)
-
end
-
-
1
def test_email(user)
-
set_language_if_valid(user.language)
-
@url = url_for(:controller => 'welcome')
-
mail :to => user.mail,
-
:subject => 'Redmine test'
-
end
-
-
# Overrides default deliver! method to prevent from sending an email
-
# with no recipient, cc or bcc
-
1
def deliver!(mail = @mail)
-
set_language_if_valid @initial_language
-
return false if (recipients.nil? || recipients.empty?) &&
-
(cc.nil? || cc.empty?) &&
-
(bcc.nil? || bcc.empty?)
-
-
-
# Log errors when raise_delivery_errors is set to false, Rails does not
-
raise_errors = self.class.raise_delivery_errors
-
self.class.raise_delivery_errors = true
-
begin
-
return super(mail)
-
rescue Exception => e
-
if raise_errors
-
raise e
-
elsif mylogger
-
mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml."
-
end
-
ensure
-
self.class.raise_delivery_errors = raise_errors
-
end
-
end
-
-
# Sends reminders to issue assignees
-
# Available options:
-
# * :days => how many days in the future to remind about (defaults to 7)
-
# * :tracker => id of tracker for filtering issues (defaults to all trackers)
-
# * :project => id or identifier of project to process (defaults to all projects)
-
# * :users => array of user/group ids who should be reminded
-
1
def self.reminders(options={})
-
days = options[:days] || 7
-
project = options[:project] ? Project.find(options[:project]) : nil
-
tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
-
user_ids = options[:users]
-
-
scope = Issue.open.where("#{Issue.table_name}.assigned_to_id IS NOT NULL" +
-
" AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
-
" AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date
-
)
-
scope = scope.where(:assigned_to_id => user_ids) if user_ids.present?
-
scope = scope.where(:project_id => project.id) if project
-
scope = scope.where(:tracker_id => tracker.id) if tracker
-
-
issues_by_assignee = scope.includes(:status, :assigned_to, :project, :tracker).all.group_by(&:assigned_to)
-
issues_by_assignee.keys.each do |assignee|
-
if assignee.is_a?(Group)
-
assignee.users.each do |user|
-
issues_by_assignee[user] ||= []
-
issues_by_assignee[user] += issues_by_assignee[assignee]
-
end
-
end
-
end
-
-
issues_by_assignee.each do |assignee, issues|
-
reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active?
-
end
-
end
-
-
# Activates/desactivates email deliveries during +block+
-
1
def self.with_deliveries(enabled = true, &block)
-
was_enabled = ActionMailer::Base.perform_deliveries
-
ActionMailer::Base.perform_deliveries = !!enabled
-
yield
-
ensure
-
ActionMailer::Base.perform_deliveries = was_enabled
-
end
-
-
# Sends emails synchronously in the given block
-
1
def self.with_synched_deliveries(&block)
-
saved_method = ActionMailer::Base.delivery_method
-
if m = saved_method.to_s.match(%r{^async_(.+)$})
-
synched_method = m[1]
-
ActionMailer::Base.delivery_method = synched_method.to_sym
-
ActionMailer::Base.send "#{synched_method}_settings=", ActionMailer::Base.send("async_#{synched_method}_settings")
-
end
-
yield
-
ensure
-
ActionMailer::Base.delivery_method = saved_method
-
end
-
-
1
def mail(headers={})
-
1003
headers.merge! 'X-Mailer' => 'Redmine',
-
'X-Redmine-Host' => Setting.host_name,
-
'X-Redmine-Site' => Setting.app_title,
-
'X-Auto-Response-Suppress' => 'OOF',
-
'Auto-Submitted' => 'auto-generated',
-
'From' => Setting.mail_from,
-
'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>"
-
-
# Removes the author from the recipients and cc
-
# if he doesn't want to receive notifications about what he does
-
1003
if @author && @author.logged? && @author.pref[:no_self_notified]
-
headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
-
headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
-
end
-
-
1003
if @author && @author.logged?
-
1003
redmine_headers 'Sender' => @author.login
-
end
-
-
# Blind carbon copy recipients
-
1003
if Setting.bcc_recipients?
-
1003
headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?)
-
1003
headers[:to] = nil
-
1003
headers[:cc] = nil
-
end
-
-
1003
if @message_id_object
-
1003
headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
-
end
-
1003
if @references_objects
-
282
headers[:references] = @references_objects.collect {|o| "<#{self.class.message_id_for(o)}>"}.join(' ')
-
end
-
-
1003
super headers do |format|
-
1003
format.text
-
1003
format.html unless Setting.plain_text_mail?
-
end
-
-
1003
set_language_if_valid @initial_language
-
end
-
-
1
def initialize(*args)
-
1003
@initial_language = current_language
-
1003
set_language_if_valid Setting.default_language
-
1003
super
-
end
-
-
1
def self.deliver_mail(mail)
-
1003
return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank?
-
1003
super
-
end
-
-
1
def self.method_missing(method, *args, &block)
-
1003
if m = method.to_s.match(%r{^deliver_(.+)$})
-
ActiveSupport::Deprecation.warn "Mailer.deliver_#{m[1]}(*args) is deprecated. Use Mailer.#{m[1]}(*args).deliver instead."
-
send(m[1], *args).deliver
-
else
-
1003
super
-
end
-
end
-
-
1
private
-
-
# Appends a Redmine header field (name is prepended with 'X-Redmine-')
-
1
def redmine_headers(h)
-
6018
h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
-
end
-
-
# Returns a predictable Message-Id for the given object
-
1
def self.message_id_for(object)
-
# id + timestamp should reduce the odds of a collision
-
# as far as we don't send multiple emails for the same object
-
1144
timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
-
1144
hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
-
1144
host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
-
1144
host = "#{::Socket.gethostname}.redmine" if host.empty?
-
1144
"#{hash}@#{host}"
-
end
-
-
1
def message_id(object)
-
1003
@message_id_object = object
-
end
-
-
1
def references(object)
-
141
@references_objects ||= []
-
141
@references_objects << object
-
end
-
-
1
def mylogger
-
Rails.logger
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Member < ActiveRecord::Base
-
1
belongs_to :user
-
1
belongs_to :principal, :foreign_key => 'user_id'
-
1
has_many :member_roles, :dependent => :destroy
-
1
has_many :roles, :through => :member_roles
-
1
belongs_to :project
-
-
1
validates_presence_of :principal, :project
-
1
validates_uniqueness_of :user_id, :scope => :project_id
-
1
validate :validate_role
-
-
1
before_destroy :set_issue_category_nil
-
1
after_destroy :unwatch_from_permission_change
-
-
1
def role
-
end
-
-
1
def role=
-
end
-
-
1
def name
-
self.user.name
-
end
-
-
1
alias :base_role_ids= :role_ids=
-
1
def role_ids=(arg)
-
ids = (arg || []).collect(&:to_i) - [0]
-
# Keep inherited roles
-
ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
-
-
new_role_ids = ids - role_ids
-
# Add new roles
-
new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) }
-
# Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
-
member_roles_to_destroy = member_roles.select {|mr| !ids.include?(mr.role_id)}
-
if member_roles_to_destroy.any?
-
member_roles_to_destroy.each(&:destroy)
-
unwatch_from_permission_change
-
end
-
end
-
-
1
def <=>(member)
-
7
a, b = roles.sort.first, member.roles.sort.first
-
7
if a == b
-
if principal
-
principal <=> member.principal
-
else
-
1
-
end
-
7
elsif a
-
7
a <=> b
-
else
-
1
-
end
-
end
-
-
1
def deletable?
-
28
member_roles.detect {|mr| mr.inherited_from}.nil?
-
end
-
-
1
def include?(user)
-
14
if principal.is_a?(Group)
-
!user.nil? && user.groups.include?(principal)
-
else
-
14
self.user == user
-
end
-
end
-
-
1
def set_issue_category_nil
-
if user
-
# remove category based auto assignments for this member
-
IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
-
end
-
end
-
-
# Find or initilize a Member with an id, attributes, and for a Principal
-
1
def self.edit_membership(id, new_attributes, principal=nil)
-
@membership = id.present? ? Member.find(id) : Member.new(:principal => principal)
-
@membership.attributes = new_attributes
-
@membership
-
end
-
-
1
protected
-
-
1
def validate_role
-
188
errors.add_on_empty :role if member_roles.empty? && roles.empty?
-
end
-
-
1
private
-
-
# Unwatch things that the user is no longer allowed to view inside project
-
1
def unwatch_from_permission_change
-
if user
-
Watcher.prune(:user => user, :project => project)
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MemberRole < ActiveRecord::Base
-
1
belongs_to :member
-
1
belongs_to :role
-
-
1
after_destroy :remove_member_if_empty
-
-
1
after_create :add_role_to_group_users
-
1
after_destroy :remove_role_from_group_users
-
-
1
validates_presence_of :role
-
1
validate :validate_role_member
-
-
1
def validate_role_member
-
304
errors.add :role_id, :invalid if role && !role.member?
-
end
-
-
1
def inherited?
-
!inherited_from.nil?
-
end
-
-
1
private
-
-
1
def remove_member_if_empty
-
if member.roles.empty?
-
member.destroy
-
end
-
end
-
-
1
def add_role_to_group_users
-
58
if member.principal.is_a?(Group)
-
member.principal.users.each do |user|
-
user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
-
user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
-
user_member.save!
-
end
-
end
-
end
-
-
1
def remove_role_from_group_users
-
MemberRole.find(:all, :conditions => { :inherited_from => id }).group_by(&:member).each do |member, member_roles|
-
member_roles.each(&:destroy)
-
if member && member.user
-
Watcher.prune(:user => member.user, :project => member.project)
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Message < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :board
-
1
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
-
1
acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
-
1
acts_as_attachable
-
1
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
-
-
acts_as_searchable :columns => ['subject', 'content'],
-
:include => {:board => :project},
-
:project_key => "#{Board.table_name}.project_id",
-
1
:date_column => "#{table_name}.created_on"
-
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
-
:description => :content,
-
:type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
-
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
-
1
{:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})}
-
-
acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
-
1
:author_key => :author_id
-
1
acts_as_watchable
-
-
1
validates_presence_of :board, :subject, :content
-
1
validates_length_of :subject, :maximum => 255
-
1
validate :cannot_reply_to_locked_topic, :on => :create
-
-
1
after_create :add_author_as_watcher, :reset_counters!
-
1
after_update :update_messages_board
-
1
after_destroy :reset_counters!
-
-
1
scope :visible, lambda {|*args| { :include => {:board => :project},
-
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
-
-
1
safe_attributes 'subject', 'content'
-
1
safe_attributes 'locked', 'sticky', 'board_id',
-
:if => lambda {|message, user|
-
user.allowed_to?(:edit_messages, message.project)
-
}
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_messages, project)
-
end
-
-
1
def cannot_reply_to_locked_topic
-
# Can not reply to a locked topic
-
errors.add :base, 'Topic is locked' if root.locked? && self != root
-
end
-
-
1
def update_messages_board
-
if board_id_changed?
-
Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
-
Board.reset_counters!(board_id_was)
-
Board.reset_counters!(board_id)
-
end
-
end
-
-
1
def reset_counters!
-
if parent && parent.id
-
Message.update_all({:last_reply_id => parent.children.maximum(:id)}, {:id => parent.id})
-
end
-
board.reset_counters!
-
end
-
-
1
def sticky=(arg)
-
write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
-
end
-
-
1
def sticky?
-
sticky == 1
-
end
-
-
1
def project
-
board.project
-
end
-
-
1
def editable_by?(usr)
-
usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
-
end
-
-
1
def destroyable_by?(usr)
-
usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
-
end
-
-
1
private
-
-
1
def add_author_as_watcher
-
Watcher.create(:watchable => self.root, :user => author)
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MessageObserver < ActiveRecord::Observer
-
1
def after_create(message)
-
Mailer.message_posted(message).deliver if Setting.notified_events.include?('message_posted')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class News < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :project
-
1
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
-
1
has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on"
-
-
1
validates_presence_of :title, :description
-
1
validates_length_of :title, :maximum => 60
-
1
validates_length_of :summary, :maximum => 255
-
-
1
acts_as_attachable :delete_permission => :manage_news
-
1
acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project
-
1
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
-
acts_as_activity_provider :find_options => {:include => [:project, :author]},
-
1
:author_key => :author_id
-
1
acts_as_watchable
-
-
1
after_create :add_author_as_watcher
-
-
1
scope :visible, lambda {|*args| {
-
:include => :project,
-
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_news, *args)
-
5
}}
-
-
1
safe_attributes 'title', 'summary', 'description'
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_news, project)
-
end
-
-
# Returns true if the news can be commented by user
-
1
def commentable?(user=User.current)
-
user.allowed_to?(:comment_news, project)
-
end
-
-
# returns latest news for projects visible by user
-
1
def self.latest(user = User.current, count = 5)
-
5
visible(user).includes([:author, :project]).order("#{News.table_name}.created_on DESC").limit(count).all
-
end
-
-
1
private
-
-
1
def add_author_as_watcher
-
Watcher.create(:watchable => self, :user => author)
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class NewsObserver < ActiveRecord::Observer
-
1
def after_create(news)
-
Mailer.news_added(news).deliver if Setting.notified_events.include?('news_added')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Principal < ActiveRecord::Base
-
1
self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
-
-
1
has_many :members, :foreign_key => 'user_id', :dependent => :destroy
-
1
has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status<>#{Project::STATUS_ARCHIVED}", :order => "#{Project.table_name}.name"
-
1
has_many :projects, :through => :memberships
-
1
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
-
-
# Groups and active users
-
1
scope :active, :conditions => "#{Principal.table_name}.status = 1"
-
-
1
scope :like, lambda {|q|
-
q = q.to_s
-
if q.blank?
-
where({})
-
else
-
pattern = "%#{q}%"
-
sql = %w(login firstname lastname mail).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ")
-
params = {:p => pattern}
-
if q =~ /^(.+)\s+(.+)$/
-
a, b = "#{$1}%", "#{$2}%"
-
sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:a) AND LOWER(#{table_name}.lastname) LIKE LOWER(:b))"
-
sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:b) AND LOWER(#{table_name}.lastname) LIKE LOWER(:a))"
-
params.merge!(:a => a, :b => b)
-
end
-
where(sql, params)
-
end
-
}
-
-
# Principals that are members of a collection of projects
-
1
scope :member_of, lambda {|projects|
-
26
projects = [projects] unless projects.is_a?(Array)
-
26
if projects.empty?
-
where("1=0")
-
else
-
26
ids = projects.map(&:id)
-
26
where("#{Principal.table_name}.status = 1 AND #{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
-
end
-
}
-
# Principals that are not members of projects
-
1
scope :not_member_of, lambda {|projects|
-
7
projects = [projects] unless projects.is_a?(Array)
-
7
if projects.empty?
-
where("1=0")
-
else
-
7
ids = projects.map(&:id)
-
7
where("#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
-
end
-
}
-
-
1
before_create :set_default_empty_values
-
-
1
def name(formatter = nil)
-
to_s
-
end
-
-
1
def <=>(principal)
-
437
if principal.nil?
-
-1
-
437
elsif self.class.name == principal.class.name
-
371
self.to_s.downcase <=> principal.to_s.downcase
-
else
-
# groups after users
-
66
principal.class.name <=> self.class.name
-
end
-
end
-
-
1
protected
-
-
# Make sure we don't try to insert NULL values (see #4632)
-
1
def set_default_empty_values
-
self.login ||= ''
-
self.hashed_password ||= ''
-
self.firstname ||= ''
-
self.lastname ||= ''
-
self.mail ||= ''
-
true
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Project < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
-
# Project statuses
-
1
STATUS_ACTIVE = 1
-
1
STATUS_CLOSED = 5
-
1
STATUS_ARCHIVED = 9
-
-
# Maximum length for project identifiers
-
1
IDENTIFIER_MAX_LENGTH = 100
-
-
# Specific overidden Activities
-
1
has_many :time_entry_activities
-
1
has_many :members, :include => [:principal, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
-
1
has_many :memberships, :class_name => 'Member'
-
1
has_many :member_principals, :class_name => 'Member',
-
:include => :principal,
-
:conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
-
1
has_many :users, :through => :members
-
1
has_many :principals, :through => :member_principals, :source => :principal
-
-
1
has_many :enabled_modules, :dependent => :delete_all
-
1
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
-
1
has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
-
1
has_many :issue_changes, :through => :issues, :source => :journals
-
1
has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
-
1
has_many :time_entries, :dependent => :delete_all
-
1
has_many :queries, :dependent => :delete_all
-
1
has_many :documents, :dependent => :destroy
-
1
has_many :news, :dependent => :destroy, :include => :author
-
1
has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
-
1
has_many :boards, :dependent => :destroy, :order => "position ASC"
-
1
has_one :repository, :conditions => ["is_default = ?", true]
-
1
has_many :repositories, :dependent => :destroy
-
1
has_many :changesets, :through => :repository
-
1
has_one :wiki, :dependent => :destroy
-
# Custom field for the project issues
-
1
has_and_belongs_to_many :issue_custom_fields,
-
:class_name => 'IssueCustomField',
-
:order => "#{CustomField.table_name}.position",
-
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
-
:association_foreign_key => 'custom_field_id'
-
-
1
acts_as_nested_set :order => 'name', :dependent => :destroy
-
acts_as_attachable :view_permission => :view_files,
-
1
:delete_permission => :manage_files
-
-
1
acts_as_customizable
-
1
acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
-
acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
-
:url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
-
1
:author => nil
-
-
1
attr_protected :status
-
-
1
validates_presence_of :name, :identifier
-
1
validates_uniqueness_of :identifier
-
1
validates_associated :repository, :wiki
-
1
validates_length_of :name, :maximum => 255
-
1
validates_length_of :homepage, :maximum => 255
-
1
validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
-
# donwcase letters, digits, dashes but not digits only
-
33
validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
-
# reserved words
-
1
validates_exclusion_of :identifier, :in => %w( new )
-
-
221
after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
-
1
before_destroy :delete_all_members
-
-
1
scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
-
1
scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
-
1
scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
-
1
scope :all_public, { :conditions => { :is_public => true } }
-
203
scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
-
1
scope :allowed_to, lambda {|*args|
-
user = User.current
-
permission = nil
-
if args.first.is_a?(Symbol)
-
permission = args.shift
-
else
-
user = args.shift
-
permission = args.shift
-
end
-
{ :conditions => Project.allowed_to_condition(user, permission, *args) }
-
}
-
1
scope :like, lambda {|arg|
-
if arg.blank?
-
{}
-
else
-
pattern = "%#{arg.to_s.strip.downcase}%"
-
{:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
-
end
-
}
-
-
1
def initialize(attributes=nil, *args)
-
32
super
-
-
32
initialized = (attributes || {}).stringify_keys
-
32
if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
-
self.identifier = Project.next_identifier
-
end
-
32
if !initialized.key?('is_public')
-
32
self.is_public = Setting.default_projects_public?
-
end
-
32
if !initialized.key?('enabled_module_names')
-
32
self.enabled_module_names = Setting.default_projects_modules
-
end
-
32
if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
-
32
self.trackers = Tracker.sorted.all
-
end
-
end
-
-
1
def identifier=(identifier)
-
32
super unless identifier_frozen?
-
end
-
-
1
def identifier_frozen?
-
46
errors[:identifier].blank? && !(new_record? || identifier.blank?)
-
end
-
-
# returns latest created projects
-
# non public projects will be returned only if user is a member of those
-
1
def self.latest(user=nil, count=5)
-
5
visible(user).find(:all, :limit => count, :order => "created_on DESC")
-
end
-
-
# Returns true if the project is visible to +user+ or to the current user.
-
1
def visible?(user=User.current)
-
1
user.allowed_to?(:view_project, self)
-
end
-
-
# Returns a SQL conditions string used to find all projects visible by the specified user.
-
#
-
# Examples:
-
# Project.visible_condition(admin) => "projects.status = 1"
-
# Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
-
# Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
-
1
def self.visible_condition(user, options={})
-
202
allowed_to_condition(user, :view_project, options)
-
end
-
-
# Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
-
#
-
# Valid options:
-
# * :project => limit the condition to project
-
# * :with_subprojects => limit the condition to project and its subprojects
-
# * :member => limit the condition to the user projects
-
1
def self.allowed_to_condition(user, permission, options={})
-
1610
perm = Redmine::AccessControl.permission(permission)
-
1610
base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
-
1610
if perm && perm.project_module
-
# If the permission belongs to a project module, make sure the module is enabled
-
1401
base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
-
end
-
1610
if options[:project]
-
project_statement = "#{Project.table_name}.id = #{options[:project].id}"
-
project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
-
base_statement = "(#{project_statement}) AND (#{base_statement})"
-
end
-
-
1610
if user.admin?
-
5
base_statement
-
else
-
1605
statement_by_role = {}
-
1605
unless options[:member]
-
1605
role = user.logged? ? Role.non_member : Role.anonymous
-
1605
if role.allowed_to?(permission)
-
1476
statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
-
end
-
end
-
1605
if user.logged?
-
1597
user.projects_by_role.each do |role, projects|
-
3194
if role.allowed_to?(permission) && projects.any?
-
3064
statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
-
end
-
end
-
end
-
1605
if statement_by_role.empty?
-
7
"1=0"
-
else
-
1598
if block_given?
-
1118
statement_by_role.each do |role, statement|
-
3354
if s = yield(role, user)
-
2236
statement_by_role[role] = "(#{statement} AND (#{s}))"
-
end
-
end
-
end
-
1598
"((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
-
end
-
end
-
end
-
-
# Returns the Systemwide and project specific activities
-
1
def activities(include_inactive=false)
-
12
if include_inactive
-
7
return all_activities
-
else
-
5
return active_activities
-
end
-
end
-
-
# Will create a new Project specific Activity or update an existing one
-
#
-
# This will raise a ActiveRecord::Rollback if the TimeEntryActivity
-
# does not successfully save.
-
1
def update_or_create_time_entry_activity(id, activity_hash)
-
if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
-
self.create_time_entry_activity_if_needed(activity_hash)
-
else
-
activity = project.time_entry_activities.find_by_id(id.to_i)
-
activity.update_attributes(activity_hash) if activity
-
end
-
end
-
-
# Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
-
#
-
# This will raise a ActiveRecord::Rollback if the TimeEntryActivity
-
# does not successfully save.
-
1
def create_time_entry_activity_if_needed(activity)
-
if activity['parent_id']
-
-
parent_activity = TimeEntryActivity.find(activity['parent_id'])
-
activity['name'] = parent_activity.name
-
activity['position'] = parent_activity.position
-
-
if Enumeration.overridding_change?(activity, parent_activity)
-
project_activity = self.time_entry_activities.create(activity)
-
-
if project_activity.new_record?
-
raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
-
else
-
self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
-
end
-
end
-
end
-
end
-
-
# Returns a :conditions SQL string that can be used to find the issues associated with this project.
-
#
-
# Examples:
-
# project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
-
# project.project_condition(false) => "projects.id = 1"
-
1
def project_condition(with_subprojects)
-
64
cond = "#{Project.table_name}.id = #{id}"
-
64
cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
-
64
cond
-
end
-
-
1
def self.find(*args)
-
2100
if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
-
1116
project = find_by_identifier(*args)
-
1116
raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
-
1116
project
-
else
-
984
super
-
end
-
end
-
-
1
def self.find_by_param(*args)
-
self.find(*args)
-
end
-
-
1
def reload(*args)
-
80
@shared_versions = nil
-
80
@rolled_up_versions = nil
-
80
@rolled_up_trackers = nil
-
80
@all_issue_custom_fields = nil
-
80
@all_time_entry_custom_fields = nil
-
80
@to_param = nil
-
80
@allowed_parents = nil
-
80
@allowed_permissions = nil
-
80
@actions_allowed = nil
-
80
super
-
end
-
-
1
def to_param
-
# id is used for projects with a numeric identifier (compatibility)
-
5753
@to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
-
end
-
-
1
def active?
-
8423
self.status == STATUS_ACTIVE
-
end
-
-
1
def archived?
-
16801
self.status == STATUS_ARCHIVED
-
end
-
-
# Archives the project and its descendants
-
1
def archive
-
# Check that there is no issue of a non descendant project that is assigned
-
# to one of the project or descendant versions
-
v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
-
if v_ids.any? && Issue.find(:first, :include => :project,
-
:conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
-
" AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
-
return false
-
end
-
Project.transaction do
-
archive!
-
end
-
true
-
end
-
-
# Unarchives the project
-
# All its ancestors must be active
-
1
def unarchive
-
return false if ancestors.detect {|a| !a.active?}
-
update_attribute :status, STATUS_ACTIVE
-
end
-
-
1
def close
-
self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
-
end
-
-
1
def reopen
-
self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
-
end
-
-
# Returns an array of projects the project can be moved to
-
# by the current user
-
1
def allowed_parents
-
7
return @allowed_parents if @allowed_parents
-
7
@allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
-
7
@allowed_parents = @allowed_parents - self_and_descendants
-
7
if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
-
7
@allowed_parents << nil
-
end
-
7
unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
-
@allowed_parents << parent
-
end
-
7
@allowed_parents
-
end
-
-
# Sets the parent of the project with authorization check
-
1
def set_allowed_parent!(p)
-
unless p.nil? || p.is_a?(Project)
-
if p.to_s.blank?
-
p = nil
-
else
-
p = Project.find_by_id(p)
-
return false unless p
-
end
-
end
-
if p.nil?
-
if !new_record? && allowed_parents.empty?
-
return false
-
end
-
elsif !allowed_parents.include?(p)
-
return false
-
end
-
set_parent!(p)
-
end
-
-
# Sets the parent of the project
-
# Argument can be either a Project, a String, a Fixnum or nil
-
1
def set_parent!(p)
-
20
unless p.nil? || p.is_a?(Project)
-
if p.to_s.blank?
-
p = nil
-
else
-
p = Project.find_by_id(p)
-
return false unless p
-
end
-
end
-
20
if p == parent && !p.nil?
-
# Nothing to do
-
true
-
20
elsif p.nil? || (p.active? && move_possible?(p))
-
20
set_or_update_position_under(p)
-
20
Issue.update_versions_from_hierarchy_change(self)
-
20
true
-
else
-
# Can not move to the given target
-
false
-
end
-
end
-
-
# Recalculates all lft and rgt values based on project names
-
# Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
-
# Used in BuildProjectsTree migration
-
1
def self.rebuild_tree!
-
transaction do
-
update_all "lft = NULL, rgt = NULL"
-
rebuild!(false)
-
end
-
end
-
-
# Returns an array of the trackers used by the project and its active sub projects
-
1
def rolled_up_trackers
-
@rolled_up_trackers ||=
-
Tracker.find(:all, :joins => :projects,
-
:select => "DISTINCT #{Tracker.table_name}.*",
-
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt],
-
88
:order => "#{Tracker.table_name}.position")
-
end
-
-
# Closes open and locked project versions that are completed
-
1
def close_completed_versions
-
Version.transaction do
-
versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
-
if version.completed?
-
version.update_attribute(:status, 'closed')
-
end
-
end
-
end
-
end
-
-
# Returns a scope of the Versions on subprojects
-
1
def rolled_up_versions
-
@rolled_up_versions ||=
-
Version.scoped(:include => :project,
-
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
-
end
-
-
# Returns a scope of the Versions used by the project
-
1
def shared_versions
-
1126
if new_record?
-
Version.scoped(:include => :project,
-
:conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
-
else
-
@shared_versions ||= begin
-
1093
r = root? ? self : root
-
1093
Version.scoped(:include => :project,
-
:conditions => "#{Project.table_name}.id = #{id}" +
-
" OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
-
" #{Version.table_name}.sharing = 'system'" +
-
" OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
-
" OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
-
" OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
-
"))")
-
1126
end
-
end
-
end
-
-
# Returns a hash of project users grouped by role
-
1
def users_by_role
-
62
members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
-
112
m.roles.each do |r|
-
112
h[r] ||= []
-
112
h[r] << m.user
-
end
-
112
h
-
end
-
end
-
-
# Deletes all project's members
-
1
def delete_all_members
-
me, mr = Member.table_name, MemberRole.table_name
-
connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
-
Member.delete_all(['project_id = ?', id])
-
end
-
-
# Users/groups issues can be assigned to
-
1
def assignable_users
-
81
assignable = Setting.issue_group_assignment? ? member_principals : members
-
567
assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
-
end
-
-
# Returns the mail adresses of users that should be always notified on project events
-
1
def recipients
-
notified_users.collect {|user| user.mail}
-
end
-
-
# Returns the users that should be notified on project events
-
1
def notified_users
-
# TODO: User part should be extracted to User#notify_about?
-
4645
members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
-
end
-
-
# Returns an array of all custom fields enabled for project issues
-
# (explictly associated custom fields and custom fields enabled for all projects)
-
1
def all_issue_custom_fields
-
1684
@all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
-
end
-
-
# Returns an array of all custom fields enabled for project time entries
-
# (explictly associated custom fields and custom fields enabled for all projects)
-
1
def all_time_entry_custom_fields
-
@all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
-
end
-
-
1
def project
-
self
-
end
-
-
1
def <=>(project)
-
name.downcase <=> project.name.downcase
-
end
-
-
1
def to_s
-
2102
name
-
end
-
-
# Returns a short description of the projects (first lines)
-
1
def short_description(length = 255)
-
21
description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
-
end
-
-
1
def css_classes
-
s = 'project'
-
s << ' root' if root?
-
s << ' child' if child?
-
s << (leaf? ? ' leaf' : ' parent')
-
unless active?
-
if archived?
-
s << ' archived'
-
else
-
s << ' closed'
-
end
-
end
-
s
-
end
-
-
# The earliest start date of a project, based on it's issues and versions
-
1
def start_date
-
[
-
issues.minimum('start_date'),
-
shared_versions.collect(&:effective_date),
-
shared_versions.collect(&:start_date)
-
].flatten.compact.min
-
end
-
-
# The latest due date of an issue or version
-
1
def due_date
-
[
-
issues.maximum('due_date'),
-
shared_versions.collect(&:effective_date),
-
shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
-
].flatten.compact.max
-
end
-
-
1
def overdue?
-
active? && !due_date.nil? && (due_date < Date.today)
-
end
-
-
# Returns the percent completed for this project, based on the
-
# progress on it's versions.
-
1
def completed_percent(options={:include_subprojects => false})
-
if options.delete(:include_subprojects)
-
total = self_and_descendants.collect(&:completed_percent).sum
-
-
total / self_and_descendants.count
-
else
-
if versions.count > 0
-
total = versions.collect(&:completed_pourcent).sum
-
-
total / versions.count
-
else
-
100
-
end
-
end
-
end
-
-
# Return true if this project allows to do the specified action.
-
# action can be:
-
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
-
# * a permission Symbol (eg. :edit_project)
-
1
def allows_to?(action)
-
7167
if archived?
-
# No action allowed on archived projects
-
return false
-
end
-
7167
unless active? || Redmine::AccessControl.read_action?(action)
-
# No write action allowed on closed projects
-
return false
-
end
-
# No action allowed on disabled modules
-
7167
if action.is_a? Hash
-
2451
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
-
else
-
4716
allowed_permissions.include? action
-
end
-
end
-
-
1
def module_enabled?(module_name)
-
281
module_name = module_name.to_s
-
2529
enabled_modules.detect {|m| m.name == module_name}
-
end
-
-
1
def enabled_module_names=(module_names)
-
32
if module_names && module_names.is_a?(Array)
-
32
module_names = module_names.collect(&:to_s).reject(&:blank?)
-
352
self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
-
else
-
enabled_modules.clear
-
end
-
end
-
-
# Returns an array of the enabled modules names
-
1
def enabled_module_names
-
3194
enabled_modules.collect(&:name)
-
end
-
-
# Enable a specific module
-
#
-
# Examples:
-
# project.enable_module!(:issue_tracking)
-
# project.enable_module!("issue_tracking")
-
1
def enable_module!(name)
-
188
enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
-
end
-
-
# Disable a module if it exists
-
#
-
# Examples:
-
# project.disable_module!(:issue_tracking)
-
# project.disable_module!("issue_tracking")
-
# project.disable_module!(project.enabled_modules.first)
-
1
def disable_module!(target)
-
target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
-
target.destroy unless target.blank?
-
end
-
-
1
safe_attributes 'name',
-
'description',
-
'homepage',
-
'is_public',
-
'identifier',
-
'custom_field_values',
-
'custom_fields',
-
'tracker_ids',
-
'issue_custom_field_ids'
-
-
1
safe_attributes 'enabled_module_names',
-
:if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
-
-
# Returns an array of projects that are in this project's hierarchy
-
#
-
# Example: parents, children, siblings
-
1
def hierarchy
-
parents = project.self_and_ancestors || []
-
descendants = project.descendants || []
-
project_hierarchy = parents | descendants # Set union
-
end
-
-
# Returns an auto-generated project identifier based on the last identifier used
-
1
def self.next_identifier
-
p = Project.find(:first, :order => 'created_on DESC')
-
p.nil? ? nil : p.identifier.to_s.succ
-
end
-
-
# Copies and saves the Project instance based on the +project+.
-
# Duplicates the source project's:
-
# * Wiki
-
# * Versions
-
# * Categories
-
# * Issues
-
# * Members
-
# * Queries
-
#
-
# Accepts an +options+ argument to specify what to copy
-
#
-
# Examples:
-
# project.copy(1) # => copies everything
-
# project.copy(1, :only => 'members') # => copies members only
-
# project.copy(1, :only => ['members', 'versions']) # => copies members and versions
-
1
def copy(project, options={})
-
project = project.is_a?(Project) ? project : Project.find(project)
-
-
to_be_copied = %w(wiki versions issue_categories issues members queries boards)
-
to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
-
-
Project.transaction do
-
if save
-
reload
-
to_be_copied.each do |name|
-
send "copy_#{name}", project
-
end
-
Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
-
save
-
end
-
end
-
end
-
-
-
# Copies +project+ and returns the new instance. This will not save
-
# the copy
-
1
def self.copy_from(project)
-
begin
-
project = project.is_a?(Project) ? project : Project.find(project)
-
if project
-
# clear unique attributes
-
attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
-
copy = Project.new(attributes)
-
copy.enabled_modules = project.enabled_modules
-
copy.trackers = project.trackers
-
copy.custom_values = project.custom_values.collect {|v| v.clone}
-
copy.issue_custom_fields = project.issue_custom_fields
-
return copy
-
else
-
return nil
-
end
-
rescue ActiveRecord::RecordNotFound
-
return nil
-
end
-
end
-
-
# Yields the given block for each project with its level in the tree
-
1
def self.project_tree(projects, &block)
-
363
ancestors = []
-
363
projects.sort_by(&:lft).each do |project|
-
1225
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
-
856
ancestors.pop
-
end
-
1225
yield project, ancestors.size
-
1225
ancestors << project
-
end
-
end
-
-
1
private
-
-
# Copies wiki from +project+
-
1
def copy_wiki(project)
-
# Check that the source project has a wiki first
-
unless project.wiki.nil?
-
wiki = self.wiki || Wiki.new
-
wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
-
wiki_pages_map = {}
-
project.wiki.pages.each do |page|
-
# Skip pages without content
-
next if page.content.nil?
-
new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
-
new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
-
new_wiki_page.content = new_wiki_content
-
wiki.pages << new_wiki_page
-
wiki_pages_map[page.id] = new_wiki_page
-
end
-
-
self.wiki = wiki
-
wiki.save
-
# Reproduce page hierarchy
-
project.wiki.pages.each do |page|
-
if page.parent_id && wiki_pages_map[page.id]
-
wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
-
wiki_pages_map[page.id].save
-
end
-
end
-
end
-
end
-
-
# Copies versions from +project+
-
1
def copy_versions(project)
-
project.versions.each do |version|
-
new_version = Version.new
-
new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
-
self.versions << new_version
-
end
-
end
-
-
# Copies issue categories from +project+
-
1
def copy_issue_categories(project)
-
project.issue_categories.each do |issue_category|
-
new_issue_category = IssueCategory.new
-
new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
-
self.issue_categories << new_issue_category
-
end
-
end
-
-
# Copies issues from +project+
-
1
def copy_issues(project)
-
# Stores the source issue id as a key and the copied issues as the
-
# value. Used to map the two togeather for issue relations.
-
issues_map = {}
-
-
# Store status and reopen locked/closed versions
-
version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
-
version_statuses.each do |version, status|
-
version.update_attribute :status, 'open'
-
end
-
-
# Get issues sorted by root_id, lft so that parent issues
-
# get copied before their children
-
project.issues.find(:all, :order => 'root_id, lft').each do |issue|
-
new_issue = Issue.new
-
new_issue.copy_from(issue, :subtasks => false, :link => false)
-
new_issue.project = self
-
# Reassign fixed_versions by name, since names are unique per project
-
if issue.fixed_version && issue.fixed_version.project == project
-
new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
-
end
-
# Reassign the category by name, since names are unique per project
-
if issue.category
-
new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
-
end
-
# Parent issue
-
if issue.parent_id
-
if copied_parent = issues_map[issue.parent_id]
-
new_issue.parent_issue_id = copied_parent.id
-
end
-
end
-
-
self.issues << new_issue
-
if new_issue.new_record?
-
logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
-
else
-
issues_map[issue.id] = new_issue unless new_issue.new_record?
-
end
-
end
-
-
# Restore locked/closed version statuses
-
version_statuses.each do |version, status|
-
version.update_attribute :status, status
-
end
-
-
# Relations after in case issues related each other
-
project.issues.each do |issue|
-
new_issue = issues_map[issue.id]
-
unless new_issue
-
# Issue was not copied
-
next
-
end
-
-
# Relations
-
issue.relations_from.each do |source_relation|
-
new_issue_relation = IssueRelation.new
-
new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
-
new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
-
if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
-
new_issue_relation.issue_to = source_relation.issue_to
-
end
-
new_issue.relations_from << new_issue_relation
-
end
-
-
issue.relations_to.each do |source_relation|
-
new_issue_relation = IssueRelation.new
-
new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
-
new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
-
if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
-
new_issue_relation.issue_from = source_relation.issue_from
-
end
-
new_issue.relations_to << new_issue_relation
-
end
-
end
-
end
-
-
# Copies members from +project+
-
1
def copy_members(project)
-
# Copy users first, then groups to handle members with inherited and given roles
-
members_to_copy = []
-
members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
-
members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
-
-
members_to_copy.each do |member|
-
new_member = Member.new
-
new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
-
# only copy non inherited roles
-
# inherited roles will be added when copying the group membership
-
role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
-
next if role_ids.empty?
-
new_member.role_ids = role_ids
-
new_member.project = self
-
self.members << new_member
-
end
-
end
-
-
# Copies queries from +project+
-
1
def copy_queries(project)
-
project.queries.each do |query|
-
new_query = ::Query.new
-
new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
-
new_query.sort_criteria = query.sort_criteria if query.sort_criteria
-
new_query.project = self
-
new_query.user_id = query.user_id
-
self.queries << new_query
-
end
-
end
-
-
# Copies boards from +project+
-
1
def copy_boards(project)
-
project.boards.each do |board|
-
new_board = Board.new
-
new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
-
new_board.project = self
-
self.boards << new_board
-
end
-
end
-
-
1
def allowed_permissions
-
@allowed_permissions ||= begin
-
19618
module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
-
135708
Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
-
5104
end
-
end
-
-
1
def allowed_actions
-
33560
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
-
end
-
-
# Returns all the active Systemwide and project specific activities
-
1
def active_activities
-
5
overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
-
-
5
if overridden_activity_ids.empty?
-
5
return TimeEntryActivity.shared.active
-
else
-
return system_activities_and_project_overrides
-
end
-
end
-
-
# Returns all the Systemwide and project specific activities
-
# (inactive and active)
-
1
def all_activities
-
7
overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
-
-
7
if overridden_activity_ids.empty?
-
7
return TimeEntryActivity.shared
-
else
-
return system_activities_and_project_overrides(true)
-
end
-
end
-
-
# Returns the systemwide active activities merged with the project specific overrides
-
1
def system_activities_and_project_overrides(include_inactive=false)
-
if include_inactive
-
return TimeEntryActivity.shared.
-
find(:all,
-
:conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
-
self.time_entry_activities
-
else
-
return TimeEntryActivity.shared.active.
-
find(:all,
-
:conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
-
self.time_entry_activities.active
-
end
-
end
-
-
# Archives subprojects recursively
-
1
def archive!
-
children.each do |subproject|
-
subproject.send :archive!
-
end
-
update_attribute :status, STATUS_ARCHIVED
-
end
-
-
1
def update_position_under_parent
-
32
set_or_update_position_under(parent)
-
end
-
-
# Inserts/moves the project so that target's children or root projects stay alphabetically sorted
-
1
def set_or_update_position_under(target_parent)
-
52
sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
-
364
to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
-
-
52
if to_be_inserted_before
-
move_to_left_of(to_be_inserted_before)
-
52
elsif target_parent.nil?
-
32
if sibs.empty?
-
# move_to_root adds the project in first (ie. left) position
-
move_to_root
-
else
-
32
move_to_right_of(sibs.last) unless self == sibs.last
-
end
-
else
-
# move_to_child_of adds the project in last (ie.right) position
-
20
move_to_child_of(target_parent)
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ProjectCustomField < CustomField
-
1
def type_name
-
:label_project_plural
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class QueryColumn
-
1
attr_accessor :name, :sortable, :groupable, :default_order
-
1
include Redmine::I18n
-
-
1
def initialize(name, options={})
-
73
self.name = name
-
73
self.sortable = options[:sortable]
-
73
self.groupable = options[:groupable] || false
-
73
if groupable == true
-
10
self.groupable = name.to_s
-
end
-
73
self.default_order = options[:default_order]
-
73
@inline = options.key?(:inline) ? options[:inline] : true
-
73
@caption_key = options[:caption] || "field_#{name}"
-
end
-
-
1
def caption
-
651
l(@caption_key)
-
end
-
-
# Returns true if the column is sortable, otherwise false
-
1
def sortable?
-
!@sortable.nil?
-
end
-
-
1
def sortable
-
571
@sortable.is_a?(Proc) ? @sortable.call : @sortable
-
end
-
-
1
def inline?
-
2370
@inline
-
end
-
-
1
def value(issue)
-
607
issue.send name
-
end
-
-
1
def css_classes
-
607
name
-
end
-
end
-
-
1
class QueryCustomFieldColumn < QueryColumn
-
-
1
def initialize(custom_field)
-
125
self.name = "cf_#{custom_field.id}".to_sym
-
125
self.sortable = custom_field.order_statement || false
-
125
self.groupable = custom_field.group_statement || false
-
125
@inline = true
-
125
@cf = custom_field
-
end
-
-
1
def caption
-
128
@cf.name
-
end
-
-
1
def custom_field
-
@cf
-
end
-
-
1
def value(issue)
-
cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
-
cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
-
end
-
-
1
def css_classes
-
@css_classes ||= "#{name} #{@cf.field_format}"
-
end
-
end
-
-
1
class Query < ActiveRecord::Base
-
1
class StatementInvalid < ::ActiveRecord::StatementInvalid
-
end
-
-
1
belongs_to :project
-
1
belongs_to :user
-
1
serialize :filters
-
1
serialize :column_names
-
1
serialize :sort_criteria, Array
-
-
1
attr_protected :project_id, :user_id
-
-
1
validates_presence_of :name
-
1
validates_length_of :name, :maximum => 255
-
1
validate :validate_query_filters
-
-
1
@@operators = { "=" => :label_equals,
-
"!" => :label_not_equals,
-
"o" => :label_open_issues,
-
"c" => :label_closed_issues,
-
"!*" => :label_none,
-
"*" => :label_any,
-
">=" => :label_greater_or_equal,
-
"<=" => :label_less_or_equal,
-
"><" => :label_between,
-
"<t+" => :label_in_less_than,
-
">t+" => :label_in_more_than,
-
"><t+"=> :label_in_the_next_days,
-
"t+" => :label_in,
-
"t" => :label_today,
-
"w" => :label_this_week,
-
">t-" => :label_less_than_ago,
-
"<t-" => :label_more_than_ago,
-
"><t-"=> :label_in_the_past_days,
-
"t-" => :label_ago,
-
"~" => :label_contains,
-
"!~" => :label_not_contains,
-
"=p" => :label_any_issues_in_project,
-
"=!p" => :label_any_issues_not_in_project,
-
"!p" => :label_no_issues_in_project}
-
-
1
cattr_reader :operators
-
-
1
@@operators_by_filter_type = { :list => [ "=", "!" ],
-
:list_status => [ "o", "=", "!", "c", "*" ],
-
:list_optional => [ "=", "!", "!*", "*" ],
-
:list_subprojects => [ "*", "!*", "=" ],
-
:date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
-
:date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
-
:string => [ "=", "~", "!", "!~", "!*", "*" ],
-
:text => [ "~", "!~", "!*", "*" ],
-
:integer => [ "=", ">=", "<=", "><", "!*", "*" ],
-
:float => [ "=", ">=", "<=", "><", "!*", "*" ],
-
:relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
-
-
1
cattr_reader :operators_by_filter_type
-
-
1
@@available_columns = [
-
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
-
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
-
QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
-
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
-
QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
-
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
-
16
QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
-
30
QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
-
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
-
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
-
16
QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
-
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
-
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
-
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
-
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
-
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
-
QueryColumn.new(:relations, :caption => :label_related_issues),
-
QueryColumn.new(:description, :inline => false)
-
]
-
1
cattr_reader :available_columns
-
-
1
scope :visible, lambda {|*args|
-
19
user = args.shift || User.current
-
19
base = Project.allowed_to_condition(user, :view_issues, *args)
-
19
user_id = user.logged? ? user.id : 0
-
{
-
:conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
-
:include => :project
-
19
}
-
}
-
-
1
def initialize(attributes=nil, *args)
-
26
super attributes
-
26
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
-
26
@is_for_all = project.nil?
-
end
-
-
1
def validate_query_filters
-
64
filters.each_key do |field|
-
144
if values_for(field)
-
144
case type_for(field)
-
when :integer
-
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
-
when :float
-
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
-
when :date, :date_past
-
case operator_for(field)
-
when "=", ">=", "<=", "><"
-
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
-
when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
-
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
-
end
-
end
-
end
-
-
add_filter_error(field, :blank) unless
-
# filter requires one or more values
-
(values_for(field) and !values_for(field).first.blank?) or
-
# filter doesn't require any value
-
144
["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
-
64
end if filters
-
end
-
-
1
def add_filter_error(field, message)
-
m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
-
errors.add(:base, m)
-
end
-
-
# Returns true if the query is visible to +user+ or the current user.
-
1
def visible?(user=User.current)
-
(project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
-
end
-
-
1
def editable_by?(user)
-
return false unless user
-
# Admin can edit them all and regular users can edit their private queries
-
return true if user.admin? || (!is_public && self.user_id == user.id)
-
# Members can not edit public queries that are for all project (only admin is allowed to)
-
is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
-
end
-
-
1
def trackers
-
77
@trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
-
end
-
-
# Returns a hash of localized labels for all filter operators
-
1
def self.operators_labels
-
400
operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
-
end
-
-
1
def available_filters
-
414
return @available_filters if @available_filters
-
26
@available_filters = {
-
"status_id" => {
-
:type => :list_status, :order => 0,
-
156
:values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] }
-
},
-
"tracker_id" => {
-
130
:type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
-
},
-
"priority_id" => {
-
156
:type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
-
},
-
"subject" => { :type => :text, :order => 8 },
-
"created_on" => { :type => :date_past, :order => 9 },
-
"updated_on" => { :type => :date_past, :order => 10 },
-
"start_date" => { :type => :date, :order => 11 },
-
"due_date" => { :type => :date, :order => 12 },
-
"estimated_hours" => { :type => :float, :order => 13 },
-
"done_ratio" => { :type => :integer, :order => 14 }
-
}
-
26
IssueRelation::TYPES.each do |relation_type, options|
-
234
@available_filters[relation_type] = {
-
:type => :relation, :order => @available_filters.size + 100,
-
:label => options[:name]
-
}
-
end
-
26
principals = []
-
26
if project
-
26
principals += project.principals.sort
-
26
unless project.leaf?
-
26
subprojects = project.descendants.visible.all
-
26
if subprojects.any?
-
26
@available_filters["subproject_id"] = {
-
:type => :list_subprojects, :order => 13,
-
104
:values => subprojects.collect{|s| [s.name, s.id.to_s] }
-
}
-
26
principals += Principal.member_of(subprojects)
-
end
-
end
-
else
-
if all_projects.any?
-
# members of visible projects
-
principals += Principal.member_of(all_projects)
-
# project filter
-
project_values = []
-
if User.current.logged? && User.current.memberships.any?
-
project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
-
end
-
project_values += all_projects_values
-
@available_filters["project_id"] = {
-
:type => :list, :order => 1, :values => project_values
-
} unless project_values.empty?
-
end
-
end
-
26
principals.uniq!
-
26
principals.sort!
-
156
users = principals.select {|p| p.is_a?(User)}
-
-
26
assigned_to_values = []
-
26
assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
-
26
assigned_to_values += (Setting.issue_group_assignment? ?
-
104
principals : users).collect{|s| [s.name, s.id.to_s] }
-
@available_filters["assigned_to_id"] = {
-
:type => :list_optional, :order => 4, :values => assigned_to_values
-
26
} unless assigned_to_values.empty?
-
-
26
author_values = []
-
26
author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
-
130
author_values += users.collect{|s| [s.name, s.id.to_s] }
-
@available_filters["author_id"] = {
-
:type => :list, :order => 5, :values => author_values
-
26
} unless author_values.empty?
-
-
78
group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
-
@available_filters["member_of_group"] = {
-
:type => :list_optional, :order => 6, :values => group_values
-
26
} unless group_values.empty?
-
-
104
role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
-
@available_filters["assigned_to_role"] = {
-
:type => :list_optional, :order => 7, :values => role_values
-
26
} unless role_values.empty?
-
-
26
if User.current.logged?
-
26
@available_filters["watcher_id"] = {
-
:type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
-
}
-
end
-
-
26
if project
-
# project specific filters
-
26
categories = project.issue_categories.all
-
26
unless categories.empty?
-
26
@available_filters["category_id"] = {
-
:type => :list_optional, :order => 6,
-
52
:values => categories.collect{|s| [s.name, s.id.to_s] }
-
}
-
end
-
26
versions = project.shared_versions.all
-
26
unless versions.empty?
-
26
@available_filters["fixed_version_id"] = {
-
:type => :list_optional, :order => 7,
-
128
:values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
-
}
-
end
-
26
add_custom_fields_filters(project.all_issue_custom_fields)
-
else
-
# global filters for cross project issue list
-
system_shared_versions = Version.visible.find_all_by_sharing('system')
-
unless system_shared_versions.empty?
-
@available_filters["fixed_version_id"] = {
-
:type => :list_optional, :order => 7,
-
:values => system_shared_versions.sort.collect{|s|
-
["#{s.project.name} - #{s.name}", s.id.to_s]
-
}
-
}
-
end
-
add_custom_fields_filters(
-
IssueCustomField.find(:all,
-
:conditions => {
-
:is_filter => true,
-
:is_for_all => true
-
}))
-
end
-
26
add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
-
if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
-
26
User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
-
26
@available_filters["is_private"] = {
-
:type => :list, :order => 16,
-
:values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
-
}
-
end
-
26
Tracker.disabled_core_fields(trackers).each {|field|
-
@available_filters.delete field
-
}
-
26
@available_filters.each do |field, options|
-
832
options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
-
end
-
26
@available_filters
-
end
-
-
# Returns a representation of the available filters for JSON serialization
-
1
def available_filters_as_json
-
16
json = {}
-
16
available_filters.each do |field, options|
-
560
json[field] = options.slice(:type, :name, :values).stringify_keys
-
end
-
16
json
-
end
-
-
1
def all_projects
-
16
@all_projects ||= Project.visible.all
-
end
-
-
1
def all_projects_values
-
16
return @all_projects_values if @all_projects_values
-
-
16
values = []
-
16
Project.project_tree(all_projects) do |p, level|
-
96
prefix = (level > 0 ? ('--' * level + ' ') : '')
-
96
values << ["#{prefix}#{p.name}", p.id.to_s]
-
end
-
16
@all_projects_values = values
-
end
-
-
1
def add_filter(field, operator, values)
-
# values must be an array
-
30
return unless values.nil? || values.is_a?(Array)
-
# check if field is defined as an available filter
-
30
if available_filters.has_key? field
-
30
filter_options = available_filters[field]
-
# check if operator is allowed for that filter
-
#if @@operators_by_filter_type[filter_options[:type]].include? operator
-
# allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
-
# filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
-
#end
-
30
filters[field] = {:operator => operator, :values => (values || [''])}
-
end
-
end
-
-
1
def add_short_filter(field, expression)
-
return unless expression && available_filters.has_key?(field)
-
field_type = available_filters[field][:type]
-
@@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
-
next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
-
add_filter field, operator, $1.present? ? $1.split('|') : ['']
-
end || add_filter(field, '=', expression.split('|'))
-
end
-
-
# Add multiple filters using +add_filter+
-
1
def add_filters(fields, operators, values)
-
if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
-
fields.each do |field|
-
add_filter(field, operators[field], values && values[field])
-
end
-
end
-
end
-
-
1
def has_filter?(field)
-
776
filters and filters[field]
-
end
-
-
1
def type_for(field)
-
158
available_filters[field][:type] if available_filters.has_key?(field)
-
end
-
-
1
def operator_for(field)
-
184
has_filter?(field) ? filters[field][:operator] : nil
-
end
-
-
1
def values_for(field)
-
560
has_filter?(field) ? filters[field][:values] : nil
-
end
-
-
1
def value_for(field, index=0)
-
(values_for(field) || [])[index]
-
end
-
-
1
def label_for(field)
-
label = available_filters[field][:name] if available_filters.has_key?(field)
-
label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
-
end
-
-
1
def available_columns
-
1763
return @available_columns if @available_columns
-
25
@available_columns = ::Query.available_columns.dup
-
25
@available_columns += (project ?
-
project.all_issue_custom_fields :
-
IssueCustomField.find(:all)
-
125
).collect {|cf| QueryCustomFieldColumn.new(cf) }
-
-
25
if User.current.allowed_to?(:view_time_entries, project, :global => true)
-
25
index = nil
-
725
@available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
-
25
index = (index ? index + 1 : -1)
-
# insert the column after estimated_hours or at the end
-
25
@available_columns.insert index, QueryColumn.new(:spent_hours,
-
:sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
-
:default_order => 'desc',
-
:caption => :label_spent_time
-
)
-
end
-
-
if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
-
25
User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
-
25
@available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
-
end
-
-
25
disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
-
25
@available_columns.reject! {|column|
-
750
disabled_fields.include?(column.name.to_s)
-
}
-
-
25
@available_columns
-
end
-
-
1
def self.available_columns=(v)
-
self.available_columns = (v)
-
end
-
-
1
def self.add_available_column(column)
-
5
self.available_columns << (column) if column.is_a?(QueryColumn)
-
end
-
-
# Returns an array of columns that can be used to group the results
-
1
def groupable_columns
-
4526
available_columns.select {|c| c.groupable}
-
end
-
-
# Returns a Hash of columns and the key for sorting
-
1
def sortable_columns
-
16
{'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
-
480
h[column.name.to_s] = column.sortable
-
480
h
-
})
-
end
-
-
1
def columns
-
# preserve the column_names order
-
251
(has_default_columns? ? default_columns_names : column_names).collect do |name|
-
11489
available_columns.find { |col| col.name == name }
-
end.compact
-
end
-
-
1
def inline_columns
-
128
columns.select(&:inline?)
-
end
-
-
1
def block_columns
-
98
columns.reject(&:inline?)
-
end
-
-
1
def available_inline_columns
-
16
available_columns.select(&:inline?)
-
end
-
-
1
def available_block_columns
-
16
available_columns.reject(&:inline?)
-
end
-
-
1
def default_columns_names
-
@default_columns_names ||= begin
-
25
default_columns = Setting.issue_list_default_columns.map(&:to_sym)
-
-
25
project.present? ? default_columns : [:project] | default_columns
-
197
end
-
end
-
-
1
def column_names=(names)
-
16
if names
-
72
names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
-
72
names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
-
# Set column_names to nil if default columns
-
9
if names == default_columns_names
-
names = nil
-
end
-
end
-
16
write_attribute(:column_names, names)
-
end
-
-
1
def has_column?(column)
-
64
column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
-
end
-
-
1
def has_default_columns?
-
251
column_names.nil? || column_names.empty?
-
end
-
-
1
def sort_criteria=(arg)
-
16
c = []
-
16
if arg.is_a?(Hash)
-
arg = arg.keys.sort.collect {|k| arg[k]}
-
end
-
48
c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
-
16
write_attribute(:sort_criteria, c)
-
end
-
-
1
def sort_criteria
-
16
read_attribute(:sort_criteria) || []
-
end
-
-
1
def sort_criteria_key(arg)
-
sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
-
end
-
-
1
def sort_criteria_order(arg)
-
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
-
end
-
-
1
def sort_criteria_order_for(key)
-
sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
-
end
-
-
# Returns the SQL sort order that should be prepended for grouping
-
1
def group_by_sort_order
-
16
if grouped? && (column = group_by_column)
-
order = sort_criteria_order_for(column.name) || column.default_order
-
column.sortable.is_a?(Array) ?
-
column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
-
"#{column.sortable} #{order}"
-
end
-
end
-
-
# Returns true if the query is a grouped query
-
1
def grouped?
-
130
!group_by_column.nil?
-
end
-
-
1
def group_by_column
-
1820
groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
-
end
-
-
1
def group_by_statement
-
group_by_column.try(:groupable)
-
end
-
-
1
def project_statement
-
32
project_clauses = []
-
32
if project && !project.descendants.active.empty?
-
32
ids = [project.id]
-
32
if has_filter?("subproject_id")
-
case operator_for("subproject_id")
-
when '='
-
# include the selected subprojects
-
ids += values_for("subproject_id").each(&:to_i)
-
when '!*'
-
# main project only
-
else
-
# all subprojects
-
ids += project.descendants.collect(&:id)
-
end
-
elsif Setting.display_subprojects_issues?
-
32
ids += project.descendants.collect(&:id)
-
end
-
32
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
-
elsif project
-
project_clauses << "#{Project.table_name}.id = %d" % project.id
-
end
-
32
project_clauses.any? ? project_clauses.join(' AND ') : nil
-
end
-
-
1
def statement
-
# filters clauses
-
32
filters_clauses = []
-
filters.each_key do |field|
-
72
next if field == "subproject_id"
-
72
v = values_for(field).clone
-
72
next unless v and !v.empty?
-
72
operator = operator_for(field)
-
-
# "me" value subsitution
-
72
if %w(assigned_to_id author_id watcher_id).include?(field)
-
if v.delete("me")
-
if User.current.logged?
-
v.push(User.current.id.to_s)
-
v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
-
else
-
v.push("0")
-
end
-
end
-
end
-
-
72
if field == 'project_id'
-
if v.delete('mine')
-
v += User.current.memberships.map(&:project_id).map(&:to_s)
-
end
-
end
-
-
72
if field =~ /cf_(\d+)$/
-
# custom field
-
filters_clauses << sql_for_custom_field(field, operator, v, $1)
-
elsif respond_to?("sql_for_#{field}_field")
-
# specific statement
-
filters_clauses << send("sql_for_#{field}_field", field, operator, v)
-
else
-
# regular field
-
72
filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
-
end
-
32
end if filters and valid?
-
-
32
filters_clauses << project_statement
-
32
filters_clauses.reject!(&:blank?)
-
-
32
filters_clauses.any? ? filters_clauses.join(' AND ') : nil
-
end
-
-
# Returns the issue count
-
1
def issue_count
-
16
Issue.visible.count(:include => [:status, :project], :conditions => statement)
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
# Returns the issue count by group or nil if query is not grouped
-
1
def issue_count_by_group
-
16
r = nil
-
16
if grouped?
-
begin
-
# Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
-
r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
-
rescue ActiveRecord::RecordNotFound
-
r = {nil => issue_count}
-
end
-
c = group_by_column
-
if c.is_a?(QueryCustomFieldColumn)
-
r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
-
end
-
end
-
16
r
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
# Returns the issues
-
# Valid options are :order, :offset, :limit, :include, :conditions
-
1
def issues(options={})
-
48
order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
-
16
order_option = nil if order_option.blank?
-
-
16
issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
-
:conditions => statement,
-
:order => order_option,
-
:joins => joins_for_order_statement(order_option),
-
:limit => options[:limit],
-
:offset => options[:offset]
-
-
16
if has_column?(:spent_hours)
-
Issue.load_visible_spent_hours(issues)
-
end
-
16
if has_column?(:relations)
-
Issue.load_visible_relations(issues)
-
end
-
16
issues
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
# Returns the issues ids
-
1
def issue_ids(options={})
-
order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
-
order_option = nil if order_option.blank?
-
-
Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
-
:conditions => statement,
-
:order => order_option,
-
:joins => joins_for_order_statement(order_option),
-
:limit => options[:limit],
-
:offset => options[:offset]).find_ids
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
# Returns the journals
-
# Valid options are :order, :offset, :limit
-
1
def journals(options={})
-
Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
-
:conditions => statement,
-
:order => options[:order],
-
:limit => options[:limit],
-
:offset => options[:offset]
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
# Returns the versions
-
# Valid options are :conditions
-
1
def versions(options={})
-
Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
1
def sql_for_watcher_id_field(field, operator, value)
-
db_table = Watcher.table_name
-
"#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
-
sql_for_field(field, '=', value, db_table, 'user_id') + ')'
-
end
-
-
1
def sql_for_member_of_group_field(field, operator, value)
-
if operator == '*' # Any group
-
groups = Group.all
-
operator = '=' # Override the operator since we want to find by assigned_to
-
elsif operator == "!*"
-
groups = Group.all
-
operator = '!' # Override the operator since we want to find by assigned_to
-
else
-
groups = Group.find_all_by_id(value)
-
end
-
groups ||= []
-
-
members_of_groups = groups.inject([]) {|user_ids, group|
-
if group && group.user_ids.present?
-
user_ids << group.user_ids
-
end
-
user_ids.flatten.uniq.compact
-
}.sort.collect(&:to_s)
-
-
'(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
-
end
-
-
1
def sql_for_assigned_to_role_field(field, operator, value)
-
case operator
-
when "*", "!*" # Member / Not member
-
sw = operator == "!*" ? 'NOT' : ''
-
nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
-
"(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
-
" WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
-
when "=", "!"
-
role_cond = value.any? ?
-
"#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
-
"1=0"
-
-
sw = operator == "!" ? 'NOT' : ''
-
nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
-
"(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
-
" WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
-
end
-
end
-
-
1
def sql_for_is_private_field(field, operator, value)
-
op = (operator == "=" ? 'IN' : 'NOT IN')
-
va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
-
-
"#{Issue.table_name}.is_private #{op} (#{va})"
-
end
-
-
1
def sql_for_relations(field, operator, value, options={})
-
relation_options = IssueRelation::TYPES[field]
-
return relation_options unless relation_options
-
-
relation_type = field
-
join_column, target_join_column = "issue_from_id", "issue_to_id"
-
if relation_options[:reverse] || options[:reverse]
-
relation_type = relation_options[:reverse] || relation_type
-
join_column, target_join_column = target_join_column, join_column
-
end
-
-
sql = case operator
-
when "*", "!*"
-
op = (operator == "*" ? 'IN' : 'NOT IN')
-
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
-
when "=", "!"
-
op = (operator == "=" ? 'IN' : 'NOT IN')
-
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
-
when "=p", "=!p", "!p"
-
op = (operator == "!p" ? 'NOT IN' : 'IN')
-
comp = (operator == "=!p" ? '<>' : '=')
-
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
-
end
-
-
if relation_options[:sym] == field && !options[:reverse]
-
sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
-
sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
-
else
-
sql
-
end
-
end
-
-
1
IssueRelation::TYPES.keys.each do |relation_type|
-
9
alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
-
end
-
-
1
private
-
-
1
def sql_for_custom_field(field, operator, value, custom_field_id)
-
db_table = CustomValue.table_name
-
db_field = 'value'
-
filter = @available_filters[field]
-
return nil unless filter
-
if filter[:format] == 'user'
-
if value.delete('me')
-
value.push User.current.id.to_s
-
end
-
end
-
not_in = nil
-
if operator == '!'
-
# Makes ! operator work for custom fields with multiple values
-
operator = '='
-
not_in = 'NOT'
-
end
-
customized_key = "id"
-
customized_class = Issue
-
if field =~ /^(.+)\.cf_/
-
assoc = $1
-
customized_key = "#{assoc}_id"
-
customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
-
raise "Unknown Issue association #{assoc}" unless customized_class
-
end
-
"#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
-
sql_for_field(field, operator, value, db_table, db_field, true) + ')'
-
end
-
-
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
-
1
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
-
52
sql = ''
-
52
case operator
-
when "="
-
14
if value.any?
-
14
case type_for(field)
-
when :date, :date_past
-
sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
-
when :integer
-
if is_custom_filter
-
sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
-
else
-
sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
-
end
-
when :float
-
if is_custom_filter
-
sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
-
else
-
sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
-
end
-
else
-
28
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
-
end
-
else
-
# IN an empty set
-
sql = "1=0"
-
end
-
when "!"
-
if value.any?
-
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
-
else
-
# NOT IN an empty set
-
sql = "1=1"
-
end
-
when "!*"
-
6
sql = "#{db_table}.#{db_field} IS NULL"
-
6
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
-
when "*"
-
12
sql = "#{db_table}.#{db_field} IS NOT NULL"
-
12
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
-
when ">="
-
if [:date, :date_past].include?(type_for(field))
-
sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
-
else
-
if is_custom_filter
-
sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
-
else
-
sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
-
end
-
end
-
when "<="
-
if [:date, :date_past].include?(type_for(field))
-
sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
-
else
-
if is_custom_filter
-
sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
-
else
-
sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
-
end
-
end
-
when "><"
-
if [:date, :date_past].include?(type_for(field))
-
sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
-
else
-
if is_custom_filter
-
sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
-
else
-
sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
-
end
-
end
-
when "o"
-
20
sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
-
when "c"
-
sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
-
when "><t-"
-
# between today - n days and today
-
sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
-
when ">t-"
-
# >= today - n days
-
sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
-
when "<t-"
-
# <= today - n days
-
sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
-
when "t-"
-
# = n days in past
-
sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
-
when "><t+"
-
# between today and today + n days
-
sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
-
when ">t+"
-
# >= today + n days
-
sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
-
when "<t+"
-
# <= today + n days
-
sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
-
when "t+"
-
# = today + n days
-
sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
-
when "t"
-
# = today
-
sql = relative_date_clause(db_table, db_field, 0, 0)
-
when "w"
-
# = this week
-
first_day_of_week = l(:general_first_day_of_week).to_i
-
day_of_week = Date.today.cwday
-
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
-
sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
-
when "~"
-
sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
-
when "!~"
-
sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
-
else
-
raise "Unknown query operator #{operator}"
-
end
-
-
52
return sql
-
end
-
-
1
def add_custom_fields_filters(custom_fields, assoc=nil)
-
52
return unless custom_fields.present?
-
52
@available_filters ||= {}
-
-
52
custom_fields.select(&:is_filter?).each do |field|
-
104
case field.field_format
-
when "text"
-
options = { :type => :text, :order => 20 }
-
when "list"
-
52
options = { :type => :list_optional, :values => field.possible_values, :order => 20}
-
when "date"
-
26
options = { :type => :date, :order => 20 }
-
when "bool"
-
options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
-
when "int"
-
options = { :type => :integer, :order => 20 }
-
when "float"
-
options = { :type => :float, :order => 20 }
-
when "user", "version"
-
next unless project
-
values = field.possible_values_options(project)
-
if User.current.logged? && field.field_format == 'user'
-
values.unshift ["<< #{l(:label_me)} >>", "me"]
-
end
-
options = { :type => :list_optional, :values => values, :order => 20}
-
else
-
26
options = { :type => :string, :order => 20 }
-
end
-
104
filter_id = "cf_#{field.id}"
-
104
filter_name = field.name
-
104
if assoc.present?
-
26
filter_id = "#{assoc}.#{filter_id}"
-
26
filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
-
end
-
104
@available_filters[filter_id] = options.merge({
-
:name => filter_name,
-
:format => field.field_format,
-
:field => field
-
})
-
end
-
end
-
-
1
def add_associations_custom_fields_filters(*associations)
-
26
fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
-
26
associations.each do |assoc|
-
104
association_klass = Issue.reflect_on_association(assoc).klass
-
104
fields_by_class.each do |field_class, fields|
-
312
if field_class.customized_class <= association_klass
-
26
add_custom_fields_filters(fields, assoc)
-
end
-
end
-
end
-
end
-
-
# Returns a SQL clause for a date or datetime field.
-
1
def date_clause(table, field, from, to)
-
s = []
-
if from
-
from_yesterday = from - 1
-
from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
-
if self.class.default_timezone == :utc
-
from_yesterday_time = from_yesterday_time.utc
-
end
-
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
-
end
-
if to
-
to_time = Time.local(to.year, to.month, to.day)
-
if self.class.default_timezone == :utc
-
to_time = to_time.utc
-
end
-
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
-
end
-
s.join(' AND ')
-
end
-
-
# Returns a SQL clause for a date or datetime field using relative dates.
-
1
def relative_date_clause(table, field, days_from, days_to)
-
date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
-
end
-
-
# Additional joins required for the given sort options
-
1
def joins_for_order_statement(order_options)
-
16
joins = []
-
-
16
if order_options
-
16
if order_options.include?('authors')
-
joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
-
end
-
16
order_options.scan(/cf_\d+/).uniq.each do |name|
-
column = available_columns.detect {|c| c.name.to_s == name}
-
join = column && column.custom_field.join_for_order_statement
-
if join
-
joins << join
-
end
-
end
-
end
-
-
16
joins.any? ? joins.join(' ') : nil
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ScmFetchError < Exception; end
-
-
1
class Repository < ActiveRecord::Base
-
1
include Redmine::Ciphering
-
1
include Redmine::SafeAttributes
-
-
# Maximum length for repository identifiers
-
1
IDENTIFIER_MAX_LENGTH = 255
-
-
1
belongs_to :project
-
1
has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
-
1
has_many :filechanges, :class_name => 'Change', :through => :changesets
-
-
1
serialize :extra_info
-
-
1
before_save :check_default
-
-
# Raw SQL to delete changesets and changes in the database
-
# has_many :changesets, :dependent => :destroy is too slow for big repositories
-
1
before_destroy :clear_changesets
-
-
1
validates_length_of :password, :maximum => 255, :allow_nil => true
-
1
validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
-
1
validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
-
1
validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
-
1
validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
-
# donwcase letters, digits, dashes, underscores but not digits only
-
1
validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :allow_blank => true
-
# Checks if the SCM is enabled when creating a repository
-
1
validate :repo_create_validation, :on => :create
-
-
1
safe_attributes 'identifier',
-
'login',
-
'password',
-
'path_encoding',
-
'log_encoding',
-
'is_default'
-
-
1
safe_attributes 'url',
-
:if => lambda {|repository, user| repository.new_record?}
-
-
1
def repo_create_validation
-
unless Setting.enabled_scm.include?(self.class.name.demodulize)
-
errors.add(:type, :invalid)
-
end
-
end
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "log_encoding"
-
attr_name = "commit_logs_encoding"
-
end
-
super(attr_name, *args)
-
end
-
-
# Removes leading and trailing whitespace
-
1
def url=(arg)
-
write_attribute(:url, arg ? arg.to_s.strip : nil)
-
end
-
-
# Removes leading and trailing whitespace
-
1
def root_url=(arg)
-
write_attribute(:root_url, arg ? arg.to_s.strip : nil)
-
end
-
-
1
def password
-
read_ciphered_attribute(:password)
-
end
-
-
1
def password=(arg)
-
write_ciphered_attribute(:password, arg)
-
end
-
-
1
def scm_adapter
-
self.class.scm_adapter_class
-
end
-
-
1
def scm
-
unless @scm
-
@scm = self.scm_adapter.new(url, root_url,
-
login, password, path_encoding)
-
if root_url.blank? && @scm.root_url.present?
-
update_attribute(:root_url, @scm.root_url)
-
end
-
end
-
@scm
-
end
-
-
1
def scm_name
-
7
self.class.scm_name
-
end
-
-
1
def name
-
if identifier.present?
-
identifier
-
elsif is_default?
-
l(:field_repository_is_default)
-
else
-
scm_name
-
end
-
end
-
-
1
def identifier=(identifier)
-
super unless identifier_frozen?
-
end
-
-
1
def identifier_frozen?
-
errors[:identifier].blank? && !(new_record? || identifier.blank?)
-
end
-
-
1
def identifier_param
-
if is_default?
-
nil
-
elsif identifier.present?
-
identifier
-
else
-
id.to_s
-
end
-
end
-
-
1
def <=>(repository)
-
if is_default?
-
-1
-
elsif repository.is_default?
-
1
-
else
-
identifier.to_s <=> repository.identifier.to_s
-
end
-
end
-
-
1
def self.find_by_identifier_param(param)
-
if param.to_s =~ /^\d+$/
-
find_by_id(param)
-
else
-
find_by_identifier(param)
-
end
-
end
-
-
1
def merge_extra_info(arg)
-
h = extra_info || {}
-
return h if arg.nil?
-
h.merge!(arg)
-
write_attribute(:extra_info, h)
-
end
-
-
1
def report_last_commit
-
true
-
end
-
-
1
def supports_cat?
-
scm.supports_cat?
-
end
-
-
1
def supports_annotate?
-
scm.supports_annotate?
-
end
-
-
1
def supports_all_revisions?
-
true
-
end
-
-
1
def supports_directory_revisions?
-
false
-
end
-
-
1
def supports_revision_graph?
-
false
-
end
-
-
1
def entry(path=nil, identifier=nil)
-
scm.entry(path, identifier)
-
end
-
-
1
def entries(path=nil, identifier=nil)
-
entries = scm.entries(path, identifier)
-
load_entries_changesets(entries)
-
entries
-
end
-
-
1
def branches
-
scm.branches
-
end
-
-
1
def tags
-
scm.tags
-
end
-
-
1
def default_branch
-
nil
-
end
-
-
1
def properties(path, identifier=nil)
-
scm.properties(path, identifier)
-
end
-
-
1
def cat(path, identifier=nil)
-
scm.cat(path, identifier)
-
end
-
-
1
def diff(path, rev, rev_to)
-
scm.diff(path, rev, rev_to)
-
end
-
-
1
def diff_format_revisions(cs, cs_to, sep=':')
-
text = ""
-
text << cs_to.format_identifier + sep if cs_to
-
text << cs.format_identifier if cs
-
text
-
end
-
-
# Returns a path relative to the url of the repository
-
1
def relative_path(path)
-
path
-
end
-
-
# Finds and returns a revision with a number or the beginning of a hash
-
1
def find_changeset_by_name(name)
-
return nil if name.blank?
-
s = name.to_s
-
changesets.find(:first, :conditions => (s.match(/^\d*$/) ?
-
["revision = ?", s] : ["revision LIKE ?", s + '%']))
-
end
-
-
1
def latest_changeset
-
@latest_changeset ||= changesets.find(:first)
-
end
-
-
# Returns the latest changesets for +path+
-
# Default behaviour is to search in cached changesets
-
1
def latest_changesets(path, rev, limit=10)
-
if path.blank?
-
changesets.find(
-
:all,
-
:include => :user,
-
:order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
-
:limit => limit)
-
else
-
filechanges.find(
-
:all,
-
:include => {:changeset => :user},
-
:conditions => ["path = ?", path.with_leading_slash],
-
:order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
-
:limit => limit
-
).collect(&:changeset)
-
end
-
end
-
-
1
def scan_changesets_for_issue_ids
-
self.changesets.each(&:scan_comment_for_issue_ids)
-
end
-
-
# Returns an array of committers usernames and associated user_id
-
1
def committers
-
@committers ||= Changeset.connection.select_rows(
-
"SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
-
end
-
-
# Maps committers username to a user ids
-
1
def committer_ids=(h)
-
if h.is_a?(Hash)
-
committers.each do |committer, user_id|
-
new_user_id = h[committer]
-
if new_user_id && (new_user_id.to_i != user_id.to_i)
-
new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
-
Changeset.update_all(
-
"user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
-
["repository_id = ? AND committer = ?", id, committer])
-
end
-
end
-
@committers = nil
-
@found_committer_users = nil
-
true
-
else
-
false
-
end
-
end
-
-
# Returns the Redmine User corresponding to the given +committer+
-
# It will return nil if the committer is not yet mapped and if no User
-
# with the same username or email was found
-
1
def find_committer_user(committer)
-
unless committer.blank?
-
@found_committer_users ||= {}
-
return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
-
-
user = nil
-
c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
-
if c && c.user
-
user = c.user
-
elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
-
username, email = $1.strip, $3
-
u = User.find_by_login(username)
-
u ||= User.find_by_mail(email) unless email.blank?
-
user = u
-
end
-
@found_committer_users[committer] = user
-
user
-
end
-
end
-
-
1
def repo_log_encoding
-
encoding = log_encoding.to_s.strip
-
encoding.blank? ? 'UTF-8' : encoding
-
end
-
-
# Fetches new changesets for all repositories of active projects
-
# Can be called periodically by an external script
-
# eg. ruby script/runner "Repository.fetch_changesets"
-
1
def self.fetch_changesets
-
Project.active.has_module(:repository).all.each do |project|
-
project.repositories.each do |repository|
-
begin
-
repository.fetch_changesets
-
rescue Redmine::Scm::Adapters::CommandFailed => e
-
logger.error "scm: error during fetching changesets: #{e.message}"
-
end
-
end
-
end
-
end
-
-
# scan changeset comments to find related and fixed issues for all repositories
-
1
def self.scan_changesets_for_issue_ids
-
find(:all).each(&:scan_changesets_for_issue_ids)
-
end
-
-
1
def self.scm_name
-
'Abstract'
-
end
-
-
1
def self.available_scm
-
subclasses.collect {|klass| [klass.scm_name, klass.name]}
-
end
-
-
1
def self.factory(klass_name, *args)
-
klass = "Repository::#{klass_name}".constantize
-
klass.new(*args)
-
rescue
-
nil
-
end
-
-
1
def self.scm_adapter_class
-
nil
-
end
-
-
1
def self.scm_command
-
ret = ""
-
begin
-
ret = self.scm_adapter_class.client_command if self.scm_adapter_class
-
rescue Exception => e
-
logger.error "scm: error during get command: #{e.message}"
-
end
-
ret
-
end
-
-
1
def self.scm_version_string
-
ret = ""
-
begin
-
ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
-
rescue Exception => e
-
logger.error "scm: error during get version string: #{e.message}"
-
end
-
ret
-
end
-
-
1
def self.scm_available
-
ret = false
-
begin
-
ret = self.scm_adapter_class.client_available if self.scm_adapter_class
-
rescue Exception => e
-
logger.error "scm: error during get scm available: #{e.message}"
-
end
-
ret
-
end
-
-
1
def set_as_default?
-
new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
-
end
-
-
1
protected
-
-
1
def check_default
-
if !is_default? && set_as_default?
-
self.is_default = true
-
end
-
if is_default? && is_default_changed?
-
Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
-
end
-
end
-
-
1
def load_entries_changesets(entries)
-
if entries
-
entries.each do |entry|
-
if entry.lastrev && entry.lastrev.identifier
-
entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
-
end
-
end
-
end
-
end
-
-
1
private
-
-
# Deletes repository data
-
1
def clear_changesets
-
cs = Changeset.table_name
-
ch = Change.table_name
-
ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
-
cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
-
-
connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
-
connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
-
connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
-
connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
-
clear_extra_info_of_changesets
-
end
-
-
1
def clear_extra_info_of_changesets
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/bazaar_adapter'
-
-
1
class Repository::Bazaar < Repository
-
1
attr_protected :root_url
-
1
validates_presence_of :url, :log_encoding
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "url"
-
attr_name = "path_to_repository"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::BazaarAdapter
-
end
-
-
1
def self.scm_name
-
'Bazaar'
-
end
-
-
1
def entry(path=nil, identifier=nil)
-
scm.bzr_path_encodig = log_encoding
-
scm.entry(path, identifier)
-
end
-
-
1
def cat(path, identifier=nil)
-
scm.bzr_path_encodig = log_encoding
-
scm.cat(path, identifier)
-
end
-
-
1
def annotate(path, identifier=nil)
-
scm.bzr_path_encodig = log_encoding
-
scm.annotate(path, identifier)
-
end
-
-
1
def diff(path, rev, rev_to)
-
scm.bzr_path_encodig = log_encoding
-
scm.diff(path, rev, rev_to)
-
end
-
-
1
def entries(path=nil, identifier=nil)
-
scm.bzr_path_encodig = log_encoding
-
entries = scm.entries(path, identifier)
-
if entries
-
entries.each do |e|
-
next if e.lastrev.revision.blank?
-
# Set the filesize unless browsing a specific revision
-
if identifier.nil? && e.is_file?
-
full_path = File.join(root_url, e.path)
-
e.size = File.stat(full_path).size if File.file?(full_path)
-
end
-
c = Change.find(
-
:first,
-
:include => :changeset,
-
:conditions => [
-
"#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?",
-
e.lastrev.revision,
-
id
-
],
-
:order => "#{Changeset.table_name}.revision DESC")
-
if c
-
e.lastrev.identifier = c.changeset.revision
-
e.lastrev.name = c.changeset.revision
-
e.lastrev.author = c.changeset.committer
-
end
-
end
-
end
-
load_entries_changesets(entries)
-
entries
-
end
-
-
1
def fetch_changesets
-
scm.bzr_path_encodig = log_encoding
-
scm_info = scm.info
-
if scm_info
-
# latest revision found in database
-
db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
-
# latest revision in the repository
-
scm_revision = scm_info.lastrev.identifier.to_i
-
if db_revision < scm_revision
-
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
-
identifier_from = db_revision + 1
-
while (identifier_from <= scm_revision)
-
# loads changesets by batches of 200
-
identifier_to = [identifier_from + 199, scm_revision].min
-
revisions = scm.revisions('', identifier_to, identifier_from)
-
transaction do
-
revisions.reverse_each do |revision|
-
changeset = Changeset.create(:repository => self,
-
:revision => revision.identifier,
-
:committer => revision.author,
-
:committed_on => revision.time,
-
:scmid => revision.scmid,
-
:comments => revision.message)
-
-
revision.paths.each do |change|
-
Change.create(:changeset => changeset,
-
:action => change[:action],
-
:path => change[:path],
-
:revision => change[:revision])
-
end
-
end
-
end unless revisions.nil?
-
identifier_from = identifier_to + 1
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/cvs_adapter'
-
1
require 'digest/sha1'
-
-
1
class Repository::Cvs < Repository
-
1
validates_presence_of :url, :root_url, :log_encoding
-
-
1
safe_attributes 'root_url',
-
:if => lambda {|repository, user| repository.new_record?}
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "root_url"
-
attr_name = "cvsroot"
-
elsif attr_name == "url"
-
attr_name = "cvs_module"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::CvsAdapter
-
end
-
-
1
def self.scm_name
-
'CVS'
-
end
-
-
1
def entry(path=nil, identifier=nil)
-
rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
-
scm.entry(path, rev.nil? ? nil : rev.committed_on)
-
end
-
-
1
def entries(path=nil, identifier=nil)
-
rev = nil
-
if ! identifier.nil?
-
rev = changesets.find_by_revision(identifier)
-
return nil if rev.nil?
-
end
-
entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
-
if entries
-
entries.each() do |entry|
-
if ( ! entry.lastrev.nil? ) && ( ! entry.lastrev.revision.nil? )
-
change = filechanges.find_by_revision_and_path(
-
entry.lastrev.revision,
-
scm.with_leading_slash(entry.path) )
-
if change
-
entry.lastrev.identifier = change.changeset.revision
-
entry.lastrev.revision = change.changeset.revision
-
entry.lastrev.author = change.changeset.committer
-
# entry.lastrev.branch = change.branch
-
end
-
end
-
end
-
end
-
load_entries_changesets(entries)
-
entries
-
end
-
-
1
def cat(path, identifier=nil)
-
rev = nil
-
if ! identifier.nil?
-
rev = changesets.find_by_revision(identifier)
-
return nil if rev.nil?
-
end
-
scm.cat(path, rev.nil? ? nil : rev.committed_on)
-
end
-
-
1
def annotate(path, identifier=nil)
-
rev = nil
-
if ! identifier.nil?
-
rev = changesets.find_by_revision(identifier)
-
return nil if rev.nil?
-
end
-
scm.annotate(path, rev.nil? ? nil : rev.committed_on)
-
end
-
-
1
def diff(path, rev, rev_to)
-
# convert rev to revision. CVS can't handle changesets here
-
diff=[]
-
changeset_from = changesets.find_by_revision(rev)
-
if rev_to.to_i > 0
-
changeset_to = changesets.find_by_revision(rev_to)
-
end
-
changeset_from.filechanges.each() do |change_from|
-
revision_from = nil
-
revision_to = nil
-
if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
-
revision_from = change_from.revision
-
end
-
if revision_from
-
if changeset_to
-
changeset_to.filechanges.each() do |change_to|
-
revision_to = change_to.revision if change_to.path == change_from.path
-
end
-
end
-
unless revision_to
-
revision_to = scm.get_previous_revision(revision_from)
-
end
-
file_diff = scm.diff(change_from.path, revision_from, revision_to)
-
diff = diff + file_diff unless file_diff.nil?
-
end
-
end
-
return diff
-
end
-
-
1
def fetch_changesets
-
# some nifty bits to introduce a commit-id with cvs
-
# natively cvs doesn't provide any kind of changesets,
-
# there is only a revision per file.
-
# we now take a guess using the author, the commitlog and the commit-date.
-
-
# last one is the next step to take. the commit-date is not equal for all
-
# commits in one changeset. cvs update the commit-date when the *,v file was touched. so
-
# we use a small delta here, to merge all changes belonging to _one_ changeset
-
time_delta = 10.seconds
-
fetch_since = latest_changeset ? latest_changeset.committed_on : nil
-
transaction do
-
tmp_rev_num = 1
-
scm.revisions('', fetch_since, nil, :log_encoding => repo_log_encoding) do |revision|
-
# only add the change to the database, if it doen't exists. the cvs log
-
# is not exclusive at all.
-
tmp_time = revision.time.clone
-
unless filechanges.find_by_path_and_revision(
-
scm.with_leading_slash(revision.paths[0][:path]),
-
revision.paths[0][:revision]
-
)
-
cmt = Changeset.normalize_comments(revision.message, repo_log_encoding)
-
author_utf8 = Changeset.to_utf8(revision.author, repo_log_encoding)
-
cs = changesets.find(
-
:first,
-
:conditions => {
-
:committed_on => tmp_time - time_delta .. tmp_time + time_delta,
-
:committer => author_utf8,
-
:comments => cmt
-
}
-
)
-
# create a new changeset....
-
unless cs
-
# we use a temporaray revision number here (just for inserting)
-
# later on, we calculate a continous positive number
-
tmp_time2 = tmp_time.clone.gmtime
-
branch = revision.paths[0][:branch]
-
scmid = branch + "-" + tmp_time2.strftime("%Y%m%d-%H%M%S")
-
cs = Changeset.create(:repository => self,
-
:revision => "tmp#{tmp_rev_num}",
-
:scmid => scmid,
-
:committer => revision.author,
-
:committed_on => tmp_time,
-
:comments => revision.message)
-
tmp_rev_num += 1
-
end
-
# convert CVS-File-States to internal Action-abbrevations
-
# default action is (M)odified
-
action = "M"
-
if revision.paths[0][:action] == "Exp" && revision.paths[0][:revision] == "1.1"
-
action = "A" # add-action always at first revision (= 1.1)
-
elsif revision.paths[0][:action] == "dead"
-
action = "D" # dead-state is similar to Delete
-
end
-
Change.create(
-
:changeset => cs,
-
:action => action,
-
:path => scm.with_leading_slash(revision.paths[0][:path]),
-
:revision => revision.paths[0][:revision],
-
:branch => revision.paths[0][:branch]
-
)
-
end
-
end
-
-
# Renumber new changesets in chronological order
-
Changeset.all(
-
:order => 'committed_on ASC, id ASC',
-
:conditions => ["repository_id = ? AND revision LIKE 'tmp%'", id]
-
).each do |changeset|
-
changeset.update_attribute :revision, next_revision_number
-
end
-
end # transaction
-
@current_revision_number = nil
-
end
-
-
1
private
-
-
# Returns the next revision number to assign to a CVS changeset
-
1
def next_revision_number
-
# Need to retrieve existing revision numbers to sort them as integers
-
sql = "SELECT revision FROM #{Changeset.table_name} "
-
sql << "WHERE repository_id = #{id} AND revision NOT LIKE 'tmp%'"
-
@current_revision_number ||= (connection.select_values(sql).collect(&:to_i).max || 0)
-
@current_revision_number += 1
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/darcs_adapter'
-
-
1
class Repository::Darcs < Repository
-
1
validates_presence_of :url, :log_encoding
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "url"
-
attr_name = "path_to_repository"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::DarcsAdapter
-
end
-
-
1
def self.scm_name
-
'Darcs'
-
end
-
-
1
def supports_directory_revisions?
-
true
-
end
-
-
1
def entry(path=nil, identifier=nil)
-
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
-
scm.entry(path, patch.nil? ? nil : patch.scmid)
-
end
-
-
1
def entries(path=nil, identifier=nil)
-
patch = nil
-
if ! identifier.nil?
-
patch = changesets.find_by_revision(identifier)
-
return nil if patch.nil?
-
end
-
entries = scm.entries(path, patch.nil? ? nil : patch.scmid)
-
if entries
-
entries.each do |entry|
-
# Search the DB for the entry's last change
-
if entry.lastrev && !entry.lastrev.scmid.blank?
-
changeset = changesets.find_by_scmid(entry.lastrev.scmid)
-
end
-
if changeset
-
entry.lastrev.identifier = changeset.revision
-
entry.lastrev.name = changeset.revision
-
entry.lastrev.time = changeset.committed_on
-
entry.lastrev.author = changeset.committer
-
end
-
end
-
end
-
load_entries_changesets(entries)
-
entries
-
end
-
-
1
def cat(path, identifier=nil)
-
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier.to_s)
-
scm.cat(path, patch.nil? ? nil : patch.scmid)
-
end
-
-
1
def diff(path, rev, rev_to)
-
patch_from = changesets.find_by_revision(rev)
-
return nil if patch_from.nil?
-
patch_to = changesets.find_by_revision(rev_to) if rev_to
-
if path.blank?
-
path = patch_from.filechanges.collect{|change| change.path}.join(' ')
-
end
-
patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil
-
end
-
-
1
def fetch_changesets
-
scm_info = scm.info
-
if scm_info
-
db_last_id = latest_changeset ? latest_changeset.scmid : nil
-
next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1
-
# latest revision in the repository
-
scm_revision = scm_info.lastrev.scmid
-
unless changesets.find_by_scmid(scm_revision)
-
revisions = scm.revisions('', db_last_id, nil, :with_path => true)
-
transaction do
-
revisions.reverse_each do |revision|
-
changeset = Changeset.create(:repository => self,
-
:revision => next_rev,
-
:scmid => revision.scmid,
-
:committer => revision.author,
-
:committed_on => revision.time,
-
:comments => revision.message)
-
revision.paths.each do |change|
-
changeset.create_change(change)
-
end
-
next_rev += 1
-
end if revisions
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# FileSystem adapter
-
# File written by Paul Rivier, at Demotera.
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/filesystem_adapter'
-
-
1
class Repository::Filesystem < Repository
-
1
attr_protected :root_url
-
1
validates_presence_of :url
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "url"
-
attr_name = "root_directory"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::FilesystemAdapter
-
end
-
-
1
def self.scm_name
-
'Filesystem'
-
end
-
-
1
def supports_all_revisions?
-
false
-
end
-
-
1
def fetch_changesets
-
nil
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
# Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/git_adapter'
-
-
1
class Repository::Git < Repository
-
1
attr_protected :root_url
-
1
validates_presence_of :url
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "url"
-
attr_name = "path_to_repository"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::GitAdapter
-
end
-
-
1
def self.scm_name
-
'Git'
-
end
-
-
1
def report_last_commit
-
extra_report_last_commit
-
end
-
-
1
def extra_report_last_commit
-
return false if extra_info.nil?
-
v = extra_info["extra_report_last_commit"]
-
return false if v.nil?
-
v.to_s != '0'
-
end
-
-
1
def supports_directory_revisions?
-
true
-
end
-
-
1
def supports_revision_graph?
-
true
-
end
-
-
1
def repo_log_encoding
-
'UTF-8'
-
end
-
-
# Returns the identifier for the given git changeset
-
1
def self.changeset_identifier(changeset)
-
changeset.scmid
-
end
-
-
# Returns the readable identifier for the given git changeset
-
1
def self.format_changeset_identifier(changeset)
-
changeset.revision[0, 8]
-
end
-
-
1
def branches
-
scm.branches
-
end
-
-
1
def tags
-
scm.tags
-
end
-
-
1
def default_branch
-
scm.default_branch
-
rescue Exception => e
-
logger.error "git: error during get default branch: #{e.message}"
-
nil
-
end
-
-
1
def find_changeset_by_name(name)
-
if name.present?
-
changesets.where(:revision => name.to_s).first ||
-
changesets.where('scmid LIKE ?', "#{name}%").first
-
end
-
end
-
-
1
def entries(path=nil, identifier=nil)
-
entries = scm.entries(path, identifier, :report_last_commit => extra_report_last_commit)
-
load_entries_changesets(entries)
-
entries
-
end
-
-
# With SCMs that have a sequential commit numbering,
-
# such as Subversion and Mercurial,
-
# Redmine is able to be clever and only fetch changesets
-
# going forward from the most recent one it knows about.
-
#
-
# However, Git does not have a sequential commit numbering.
-
#
-
# In order to fetch only new adding revisions,
-
# Redmine needs to save "heads".
-
#
-
# In Git and Mercurial, revisions are not in date order.
-
# Redmine Mercurial fixed issues.
-
# * Redmine Takes Too Long On Large Mercurial Repository
-
# http://www.redmine.org/issues/3449
-
# * Sorting for changesets might go wrong on Mercurial repos
-
# http://www.redmine.org/issues/3567
-
#
-
# Database revision column is text, so Redmine can not sort by revision.
-
# Mercurial has revision number, and revision number guarantees revision order.
-
# Redmine Mercurial model stored revisions ordered by database id to database.
-
# So, Redmine Mercurial model can use correct ordering revisions.
-
#
-
# Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10"
-
# to get limited revisions from old to new.
-
# But, Git 1.7.3.4 does not support --reverse with -n or --skip.
-
#
-
# The repository can still be fully reloaded by calling #clear_changesets
-
# before fetching changesets (eg. for offline resync)
-
1
def fetch_changesets
-
scm_brs = branches
-
return if scm_brs.nil? || scm_brs.empty?
-
-
h1 = extra_info || {}
-
h = h1.dup
-
repo_heads = scm_brs.map{ |br| br.scmid }
-
h["heads"] ||= []
-
prev_db_heads = h["heads"].dup
-
if prev_db_heads.empty?
-
prev_db_heads += heads_from_branches_hash
-
end
-
return if prev_db_heads.sort == repo_heads.sort
-
-
h["db_consistent"] ||= {}
-
if changesets.count == 0
-
h["db_consistent"]["ordering"] = 1
-
merge_extra_info(h)
-
self.save
-
elsif ! h["db_consistent"].has_key?("ordering")
-
h["db_consistent"]["ordering"] = 0
-
merge_extra_info(h)
-
self.save
-
end
-
save_revisions(prev_db_heads, repo_heads)
-
end
-
-
1
def save_revisions(prev_db_heads, repo_heads)
-
h = {}
-
opts = {}
-
opts[:reverse] = true
-
opts[:excludes] = prev_db_heads
-
opts[:includes] = repo_heads
-
-
revisions = scm.revisions('', nil, nil, opts)
-
return if revisions.blank?
-
-
# Make the search for existing revisions in the database in a more sufficient manner
-
#
-
# Git branch is the reference to the specific revision.
-
# Git can *delete* remote branch and *re-push* branch.
-
#
-
# $ git push remote :branch
-
# $ git push remote branch
-
#
-
# After deleting branch, revisions remain in repository until "git gc".
-
# On git 1.7.2.3, default pruning date is 2 weeks.
-
# So, "git log --not deleted_branch_head_revision" return code is 0.
-
#
-
# After re-pushing branch, "git log" returns revisions which are saved in database.
-
# So, Redmine needs to scan revisions and database every time.
-
#
-
# This is replacing the one-after-one queries.
-
# Find all revisions, that are in the database, and then remove them from the revision array.
-
# Then later we won't need any conditions for db existence.
-
# Query for several revisions at once, and remove them from the revisions array, if they are there.
-
# Do this in chunks, to avoid eventual memory problems (in case of tens of thousands of commits).
-
# If there are no revisions (because the original code's algorithm filtered them),
-
# then this part will be stepped over.
-
# We make queries, just if there is any revision.
-
limit = 100
-
offset = 0
-
revisions_copy = revisions.clone # revisions will change
-
while offset < revisions_copy.size
-
recent_changesets_slice = changesets.find(
-
:all,
-
:conditions => [
-
'scmid IN (?)',
-
revisions_copy.slice(offset, limit).map{|x| x.scmid}
-
]
-
)
-
# Subtract revisions that redmine already knows about
-
recent_revisions = recent_changesets_slice.map{|c| c.scmid}
-
revisions.reject!{|r| recent_revisions.include?(r.scmid)}
-
offset += limit
-
end
-
-
revisions.each do |rev|
-
transaction do
-
# There is no search in the db for this revision, because above we ensured,
-
# that it's not in the db.
-
save_revision(rev)
-
end
-
end
-
h["heads"] = repo_heads.dup
-
merge_extra_info(h)
-
self.save
-
end
-
1
private :save_revisions
-
-
1
def save_revision(rev)
-
parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
-
changeset = Changeset.create(
-
:repository => self,
-
:revision => rev.identifier,
-
:scmid => rev.scmid,
-
:committer => rev.author,
-
:committed_on => rev.time,
-
:comments => rev.message,
-
:parents => parents
-
)
-
unless changeset.new_record?
-
rev.paths.each { |change| changeset.create_change(change) }
-
end
-
changeset
-
end
-
1
private :save_revision
-
-
1
def heads_from_branches_hash
-
h1 = extra_info || {}
-
h = h1.dup
-
h["branches"] ||= {}
-
h['branches'].map{|br, hs| hs['last_scmid']}
-
end
-
-
1
def latest_changesets(path,rev,limit=10)
-
revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
-
return [] if revisions.nil? || revisions.empty?
-
-
changesets.find(
-
:all,
-
:conditions => [
-
"scmid IN (?)",
-
revisions.map!{|c| c.scmid}
-
],
-
:order => 'committed_on DESC'
-
)
-
end
-
-
1
def clear_extra_info_of_changesets
-
return if extra_info.nil?
-
v = extra_info["extra_report_last_commit"]
-
write_attribute(:extra_info, nil)
-
h = {}
-
h["extra_report_last_commit"] = v
-
merge_extra_info(h)
-
self.save
-
end
-
1
private :clear_extra_info_of_changesets
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/mercurial_adapter'
-
-
1
class Repository::Mercurial < Repository
-
# sort changesets by revision number
-
1
has_many :changesets,
-
:order => "#{Changeset.table_name}.id DESC",
-
:foreign_key => 'repository_id'
-
-
1
attr_protected :root_url
-
1
validates_presence_of :url
-
-
# number of changesets to fetch at once
-
1
FETCH_AT_ONCE = 100
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "url"
-
attr_name = "path_to_repository"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::MercurialAdapter
-
end
-
-
1
def self.scm_name
-
'Mercurial'
-
end
-
-
1
def supports_directory_revisions?
-
true
-
end
-
-
1
def supports_revision_graph?
-
true
-
end
-
-
1
def repo_log_encoding
-
'UTF-8'
-
end
-
-
# Returns the readable identifier for the given mercurial changeset
-
1
def self.format_changeset_identifier(changeset)
-
"#{changeset.revision}:#{changeset.scmid}"
-
end
-
-
# Returns the identifier for the given Mercurial changeset
-
1
def self.changeset_identifier(changeset)
-
changeset.scmid
-
end
-
-
1
def diff_format_revisions(cs, cs_to, sep=':')
-
super(cs, cs_to, ' ')
-
end
-
-
# Finds and returns a revision with a number or the beginning of a hash
-
1
def find_changeset_by_name(name)
-
return nil if name.blank?
-
s = name.to_s
-
if /[^\d]/ =~ s or s.size > 8
-
cs = changesets.where(:scmid => s).first
-
else
-
cs = changesets.where(:revision => s).first
-
end
-
return cs if cs
-
changesets.where('scmid LIKE ?', "#{s}%").first
-
end
-
-
# Returns the latest changesets for +path+; sorted by revision number
-
#
-
# Because :order => 'id DESC' is defined at 'has_many',
-
# there is no need to set 'order'.
-
# But, MySQL test fails.
-
# Sqlite3 and PostgreSQL pass.
-
# Is this MySQL bug?
-
1
def latest_changesets(path, rev, limit=10)
-
changesets.find(:all,
-
:include => :user,
-
:conditions => latest_changesets_cond(path, rev, limit),
-
:limit => limit,
-
:order => "#{Changeset.table_name}.id DESC")
-
end
-
-
1
def latest_changesets_cond(path, rev, limit)
-
cond, args = [], []
-
if scm.branchmap.member? rev
-
# Mercurial named branch is *stable* in each revision.
-
# So, named branch can be stored in database.
-
# Mercurial provides *bookmark* which is equivalent with git branch.
-
# But, bookmark is not implemented.
-
cond << "#{Changeset.table_name}.scmid IN (?)"
-
# Revisions in root directory and sub directory are not equal.
-
# So, in order to get correct limit, we need to get all revisions.
-
# But, it is very heavy.
-
# Mercurial does not treat direcotry.
-
# So, "hg log DIR" is very heavy.
-
branch_limit = path.blank? ? limit : ( limit * 5 )
-
args << scm.nodes_in_branch(rev, :limit => branch_limit)
-
elsif last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
-
cond << "#{Changeset.table_name}.id <= ?"
-
args << last.id
-
end
-
unless path.blank?
-
cond << "EXISTS (SELECT * FROM #{Change.table_name}
-
WHERE #{Change.table_name}.changeset_id = #{Changeset.table_name}.id
-
AND (#{Change.table_name}.path = ?
-
OR #{Change.table_name}.path LIKE ? ESCAPE ?))"
-
args << path.with_leading_slash
-
args << "#{path.with_leading_slash.gsub(%r{[%_\\]}) { |s| "\\#{s}" }}/%" << '\\'
-
end
-
[cond.join(' AND '), *args] unless cond.empty?
-
end
-
1
private :latest_changesets_cond
-
-
1
def fetch_changesets
-
return if scm.info.nil?
-
scm_rev = scm.info.lastrev.revision.to_i
-
db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
-
return unless db_rev < scm_rev # already up-to-date
-
-
logger.debug "Fetching changesets for repository #{url}" if logger
-
(db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i|
-
scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re|
-
transaction do
-
parents = (re.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
-
cs = Changeset.create(:repository => self,
-
:revision => re.revision,
-
:scmid => re.scmid,
-
:committer => re.author,
-
:committed_on => re.time,
-
:comments => re.message,
-
:parents => parents)
-
unless cs.new_record?
-
re.paths.each { |e| cs.create_change(e) }
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/subversion_adapter'
-
-
1
class Repository::Subversion < Repository
-
1
attr_protected :root_url
-
1
validates_presence_of :url
-
1
validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::SubversionAdapter
-
end
-
-
1
def self.scm_name
-
7
'Subversion'
-
end
-
-
1
def supports_directory_revisions?
-
true
-
end
-
-
1
def repo_log_encoding
-
'UTF-8'
-
end
-
-
1
def latest_changesets(path, rev, limit=10)
-
revisions = scm.revisions(path, rev, nil, :limit => limit)
-
if revisions
-
identifiers = revisions.collect(&:identifier).compact
-
changesets.where(:revision => identifiers).reorder("committed_on DESC").includes(:repository, :user).all
-
else
-
[]
-
end
-
end
-
-
# Returns a path relative to the url of the repository
-
1
def relative_path(path)
-
path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
-
end
-
-
1
def fetch_changesets
-
scm_info = scm.info
-
if scm_info
-
# latest revision found in database
-
db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
-
# latest revision in the repository
-
scm_revision = scm_info.lastrev.identifier.to_i
-
if db_revision < scm_revision
-
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
-
identifier_from = db_revision + 1
-
while (identifier_from <= scm_revision)
-
# loads changesets by batches of 200
-
identifier_to = [identifier_from + 199, scm_revision].min
-
revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
-
revisions.reverse_each do |revision|
-
transaction do
-
changeset = Changeset.create(:repository => self,
-
:revision => revision.identifier,
-
:committer => revision.author,
-
:committed_on => revision.time,
-
:comments => revision.message)
-
-
revision.paths.each do |change|
-
changeset.create_change(change)
-
end unless changeset.new_record?
-
end
-
end unless revisions.nil?
-
identifier_from = identifier_to + 1
-
end
-
end
-
end
-
end
-
-
1
protected
-
-
1
def load_entries_changesets(entries)
-
return unless entries
-
-
entries_with_identifier = entries.select {|entry| entry.lastrev && entry.lastrev.identifier.present?}
-
identifiers = entries_with_identifier.map {|entry| entry.lastrev.identifier}.compact.uniq
-
-
if identifiers.any?
-
changesets_by_identifier = changesets.where(:revision => identifiers).includes(:user, :repository).all.group_by(&:revision)
-
entries_with_identifier.each do |entry|
-
if m = changesets_by_identifier[entry.lastrev.identifier]
-
entry.changeset = m.first
-
end
-
end
-
end
-
end
-
-
1
private
-
-
# Returns the relative url of the repository
-
# Eg: root_url = file:///var/svn/foo
-
# url = file:///var/svn/foo/bar
-
# => returns /bar
-
1
def relative_url
-
@relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url || scm.root_url)}", Regexp::IGNORECASE), '')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Role < ActiveRecord::Base
-
# Custom coder for the permissions attribute that should be an
-
# array of symbols. Rails 3 uses Psych which can be *unbelievably*
-
# slow on some platforms (eg. mingw32).
-
1
class PermissionsAttributeCoder
-
1
def self.load(str)
-
5062
str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
-
end
-
-
1
def self.dump(value)
-
120
YAML.dump(value)
-
end
-
end
-
-
# Built-in roles
-
1
BUILTIN_NON_MEMBER = 1
-
1
BUILTIN_ANONYMOUS = 2
-
-
1
ISSUES_VISIBILITY_OPTIONS = [
-
['all', :label_issues_visibility_all],
-
['default', :label_issues_visibility_public],
-
['own', :label_issues_visibility_own]
-
]
-
-
1
scope :sorted, order("#{table_name}.builtin ASC, #{table_name}.position ASC")
-
1
scope :givable, order("#{table_name}.position ASC").where(:builtin => 0)
-
1
scope :builtin, lambda { |*args|
-
compare = (args.first == true ? 'not' : '')
-
where("#{compare} builtin = 0")
-
}
-
-
1
before_destroy :check_deletable
-
1
has_many :workflow_rules, :dependent => :delete_all do
-
1
def copy(source_role)
-
WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
-
end
-
end
-
-
1
has_many :member_roles, :dependent => :destroy
-
1
has_many :members, :through => :member_roles
-
1
acts_as_list
-
-
1
serialize :permissions, ::Role::PermissionsAttributeCoder
-
1
attr_protected :builtin
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name
-
1
validates_length_of :name, :maximum => 30
-
1
validates_inclusion_of :issues_visibility,
-
:in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
-
308
:if => lambda {|role| role.respond_to?(:issues_visibility)}
-
-
# Copies attributes from another role, arg can be an id or a Role
-
1
def copy_from(arg, options={})
-
return unless arg.present?
-
role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s)
-
self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions")
-
self.permissions = role.permissions.dup
-
self
-
end
-
-
1
def permissions=(perms)
-
perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
-
write_attribute(:permissions, perms)
-
end
-
-
1
def add_permission!(*perms)
-
self.permissions = [] unless permissions.is_a?(Array)
-
-
permissions_will_change!
-
perms.each do |p|
-
p = p.to_sym
-
permissions << p unless permissions.include?(p)
-
end
-
save!
-
end
-
-
1
def remove_permission!(*perms)
-
return unless permissions.is_a?(Array)
-
permissions_will_change!
-
perms.each { |p| permissions.delete(p.to_sym) }
-
save!
-
end
-
-
# Returns true if the role has the given permission
-
1
def has_permission?(perm)
-
!permissions.nil? && permissions.include?(perm.to_sym)
-
end
-
-
1
def <=>(role)
-
57
if role
-
57
if builtin == role.builtin
-
57
position <=> role.position
-
else
-
builtin <=> role.builtin
-
end
-
else
-
-1
-
end
-
end
-
-
1
def to_s
-
189
name
-
end
-
-
1
def name
-
1191
case builtin
-
when 1; l(:label_role_non_member, :default => read_attribute(:name))
-
when 2; l(:label_role_anonymous, :default => read_attribute(:name))
-
1191
else; read_attribute(:name)
-
end
-
end
-
-
# Return true if the role is a builtin role
-
1
def builtin?
-
449
self.builtin != 0
-
end
-
-
# Return true if the role is the anonymous role
-
1
def anonymous?
-
builtin == 2
-
end
-
-
# Return true if the role is a project member role
-
1
def member?
-
449
!self.builtin?
-
end
-
-
# Return true if role is allowed to do the specified action
-
# action can be:
-
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
-
# * a permission Symbol (eg. :edit_project)
-
1
def allowed_to?(action)
-
13399
if action.is_a? Hash
-
3654
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
-
else
-
9745
allowed_permissions.include? action
-
end
-
end
-
-
# Return all the permissions that can be given to the role
-
1
def setable_permissions
-
setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
-
setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
-
setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
-
setable_permissions
-
end
-
-
# Find all the roles that can be given to a project member
-
1
def self.find_all_givable
-
7
Role.givable.all
-
end
-
-
# Return the builtin 'non member' role. If the role doesn't exist,
-
# it will be created on the fly.
-
1
def self.non_member
-
2047
find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
-
end
-
-
# Return the builtin 'anonymous' role. If the role doesn't exist,
-
# it will be created on the fly.
-
1
def self.anonymous
-
264
find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
-
end
-
-
1
private
-
-
1
def allowed_permissions
-
30755
@allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
-
end
-
-
1
def allowed_actions
-
55277
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
-
end
-
-
1
def check_deletable
-
raise "Can't delete role" if members.any?
-
raise "Can't delete builtin role" if builtin?
-
end
-
-
1
def self.find_or_create_system_role(builtin, name)
-
2311
role = where(:builtin => builtin).first
-
2311
if role.nil?
-
role = create(:name => name, :position => 0) do |r|
-
r.builtin = builtin
-
end
-
raise "Unable to create the #{name} role." if role.new_record?
-
end
-
2311
role
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Setting < ActiveRecord::Base
-
-
1
DATE_FORMATS = [
-
'%Y-%m-%d',
-
'%d/%m/%Y',
-
'%d.%m.%Y',
-
'%d-%m-%Y',
-
'%m/%d/%Y',
-
'%d %b %Y',
-
'%d %B %Y',
-
'%b %d, %Y',
-
'%B %d, %Y'
-
]
-
-
1
TIME_FORMATS = [
-
'%H:%M',
-
'%I:%M %p'
-
]
-
-
1
ENCODINGS = %w(US-ASCII
-
windows-1250
-
windows-1251
-
windows-1252
-
windows-1253
-
windows-1254
-
windows-1255
-
windows-1256
-
windows-1257
-
windows-1258
-
windows-31j
-
ISO-2022-JP
-
ISO-2022-KR
-
ISO-8859-1
-
ISO-8859-2
-
ISO-8859-3
-
ISO-8859-4
-
ISO-8859-5
-
ISO-8859-6
-
ISO-8859-7
-
ISO-8859-8
-
ISO-8859-9
-
ISO-8859-13
-
ISO-8859-15
-
KOI8-R
-
UTF-8
-
UTF-16
-
UTF-16BE
-
UTF-16LE
-
EUC-JP
-
Shift_JIS
-
CP932
-
GB18030
-
GBK
-
ISCII91
-
EUC-KR
-
Big5
-
Big5-HKSCS
-
TIS-620)
-
-
1
cattr_accessor :available_settings
-
1
@@available_settings = YAML::load(File.open("#{Rails.root}/config/settings.yml"))
-
1
Redmine::Plugin.all.each do |plugin|
-
1
next unless plugin.settings
-
1
@@available_settings["plugin_#{plugin.id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
-
end
-
-
1
validates_uniqueness_of :name
-
1
validates_inclusion_of :name, :in => @@available_settings.keys
-
987
validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting| @@available_settings[setting.name]['format'] == 'int' }
-
-
# Hash used to cache setting values
-
1
@cached_settings = {}
-
1
@cached_cleared_on = Time.now
-
-
1
def value
-
5260
v = read_attribute(:value)
-
# Unserialize serialized settings
-
5260
v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String)
-
5260
v = v.to_sym if @@available_settings[name]['format'] == 'symbol' && !v.blank?
-
5260
v
-
end
-
-
1
def value=(v)
-
3969
v = v.to_yaml if v && @@available_settings[name] && @@available_settings[name]['serialized']
-
3969
write_attribute(:value, v.to_s)
-
end
-
-
# Returns the value of the setting named name
-
1
def self.[](name)
-
262421
v = @cached_settings[name]
-
262421
v ? v : (@cached_settings[name] = find_or_default(name).value)
-
end
-
-
1
def self.[]=(name, v)
-
986
setting = find_or_default(name)
-
986
setting.value = (v ? v : "")
-
986
@cached_settings[name] = nil
-
986
setting.save
-
986
setting.value
-
end
-
-
# Defines getter and setter for each setting
-
# Then setting values can be read using: Setting.some_setting_name
-
# or set using Setting.some_setting_name = "some value"
-
1
@@available_settings.each do |name, params|
-
73
src = <<-END_SRC
-
def self.#{name}
-
self[:#{name}]
-
end
-
-
def self.#{name}?
-
self[:#{name}].to_i > 0
-
end
-
-
def self.#{name}=(value)
-
self[:#{name}] = value
-
end
-
END_SRC
-
73
class_eval src, __FILE__, __LINE__
-
end
-
-
# Helper that returns an array based on per_page_options setting
-
1
def self.per_page_options_array
-
148
per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
-
end
-
-
1
def self.openid?
-
242
Object.const_defined?(:OpenID) && self[:openid].to_i > 0
-
end
-
-
# Checks if settings have changed since the values were read
-
# and clears the cache hash if it's the case
-
# Called once per request
-
1
def self.check_cache
-
841
settings_updated_on = Setting.maximum(:updated_on)
-
841
if settings_updated_on && @cached_cleared_on <= settings_updated_on
-
129
clear_cache
-
end
-
end
-
-
# Clears the settings cache
-
1
def self.clear_cache
-
129
@cached_settings.clear
-
129
@cached_cleared_on = Time.now
-
129
logger.info "Settings cache cleared." if logger
-
end
-
-
1
private
-
# Returns the Setting instance for the setting named name
-
# (record found in database or new record with default value)
-
1
def self.find_or_default(name)
-
5054
name = name.to_s
-
5054
raise "There's no setting named #{name}" unless @@available_settings.has_key?(name)
-
5054
setting = find_by_name(name)
-
5054
unless setting
-
2983
setting = new(:name => name)
-
2983
setting.value = @@available_settings[name]['default']
-
end
-
5054
setting
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TimeEntry < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
# could have used polymorphic association
-
# project association here allows easy loading of time entries at project level with one database trip
-
1
belongs_to :project
-
1
belongs_to :issue
-
1
belongs_to :user
-
1
belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id'
-
-
1
attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
-
-
1
acts_as_customizable
-
acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
-
:url => Proc.new {|o| {:controller => 'timelog', :action => 'index', :project_id => o.project, :issue_id => o.issue}},
-
:author => :user,
-
1
:description => :comments
-
-
acts_as_activity_provider :timestamp => "#{table_name}.created_on",
-
:author_key => :user_id,
-
1
:find_options => {:include => :project}
-
-
1
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
-
1
validates_numericality_of :hours, :allow_nil => true, :message => :invalid
-
1
validates_length_of :comments, :maximum => 255, :allow_nil => true
-
1
before_validation :set_project_if_nil
-
1
validate :validate_time_entry
-
-
1
scope :visible, lambda {|*args| {
-
:include => :project,
-
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_time_entries, *args)
-
66
}}
-
1
scope :on_issue, lambda {|issue| {
-
:include => :issue,
-
:conditions => "#{Issue.table_name}.root_id = #{issue.root_id} AND #{Issue.table_name}.lft >= #{issue.lft} AND #{Issue.table_name}.rgt <= #{issue.rgt}"
-
2
}}
-
1
scope :on_project, lambda {|project, include_subprojects| {
-
:include => :project,
-
:conditions => project.project_condition(include_subprojects)
-
2
}}
-
1
scope :spent_between, lambda {|from, to|
-
4
if from && to
-
{:conditions => ["#{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", from, to]}
-
elsif from
-
{:conditions => ["#{TimeEntry.table_name}.spent_on >= ?", from]}
-
elsif to
-
{:conditions => ["#{TimeEntry.table_name}.spent_on <= ?", to]}
-
else
-
4
{}
-
end
-
}
-
-
1
safe_attributes 'hours', 'comments', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields'
-
-
1
def initialize(attributes=nil, *args)
-
7
super
-
7
if new_record? && self.activity.nil?
-
7
if default_activity = TimeEntryActivity.default
-
7
self.activity_id = default_activity.id
-
end
-
7
self.hours = nil if hours == 0
-
end
-
end
-
-
1
def set_project_if_nil
-
3
self.project = issue.project if issue && project.nil?
-
end
-
-
1
def validate_time_entry
-
3
errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
-
3
errors.add :project_id, :invalid if project.nil?
-
3
errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
-
end
-
-
1
def hours=(h)
-
2
write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
-
end
-
-
1
def hours
-
28
h = read_attribute(:hours)
-
28
if h.is_a?(Float)
-
21
h.round(2)
-
else
-
7
h
-
end
-
end
-
-
# tyear, tmonth, tweek assigned where setting spent_on attributes
-
# these attributes make time aggregations easier
-
1
def spent_on=(date)
-
6
super
-
6
if spent_on.is_a?(Time)
-
self.spent_on = spent_on.to_date
-
end
-
6
self.tyear = spent_on ? spent_on.year : nil
-
6
self.tmonth = spent_on ? spent_on.month : nil
-
6
self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
-
end
-
-
# Returns true if the time entry can be edited by usr, otherwise false
-
1
def editable_by?(usr)
-
4
(usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TimeEntryActivity < Enumeration
-
1
has_many :time_entries, :foreign_key => 'activity_id'
-
-
1
OptionName = :enumeration_activities
-
-
1
def option_name
-
OptionName
-
end
-
-
1
def objects_count
-
time_entries.count
-
end
-
-
1
def transfer_relations(to)
-
time_entries.update_all("activity_id = #{to.id}")
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TimeEntryActivityCustomField < CustomField
-
1
def type_name
-
:enumeration_activities
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TimeEntryCustomField < CustomField
-
1
def type_name
-
:label_spent_time
-
end
-
end
-
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Token < ActiveRecord::Base
-
1
belongs_to :user
-
1
validates_uniqueness_of :value
-
-
1
before_create :delete_previous_tokens, :generate_new_token
-
-
1
@@validity_time = 1.day
-
-
1
def generate_new_token
-
130
self.value = Token.generate_token_value
-
end
-
-
# Return true if token has expired
-
1
def expired?
-
return Time.now > self.created_on + @@validity_time
-
end
-
-
# Delete all expired tokens
-
1
def self.destroy_expired
-
Token.delete_all ["action NOT IN (?) AND created_on < ?", ['feeds', 'api'], Time.now - @@validity_time]
-
end
-
-
1
private
-
1
def self.generate_token_value
-
130
Redmine::Utils.random_hex(20)
-
end
-
-
# Removes obsolete tokens (same user and action)
-
1
def delete_previous_tokens
-
130
if user
-
130
Token.delete_all(['user_id = ? AND action = ?', user.id, action])
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Tracker < ActiveRecord::Base
-
-
1
CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze
-
# Fields that can be disabled
-
# Other (future) fields should be appended, not inserted!
-
1
CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze
-
1
CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze
-
-
1
before_destroy :check_integrity
-
1
has_many :issues
-
1
has_many :workflow_rules, :dependent => :delete_all do
-
1
def copy(source_tracker)
-
376
WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil)
-
end
-
end
-
-
1
has_and_belongs_to_many :projects
-
1
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
-
1
acts_as_list
-
-
1
attr_protected :field_bits
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name
-
1
validates_length_of :name, :maximum => 30
-
-
1
scope :sorted, order("#{table_name}.position ASC")
-
1
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
-
-
866
def to_s; name end
-
-
1
def <=>(tracker)
-
1
position <=> tracker.position
-
end
-
-
# Returns an array of IssueStatus that are used
-
# in the tracker's workflows
-
1
def issue_statuses
-
117
if @issue_statuses
-
47
return @issue_statuses
-
elsif new_record?
-
return []
-
end
-
-
70
ids = WorkflowTransition.
-
connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE tracker_id = #{id} AND type = 'WorkflowTransition'").
-
flatten.
-
uniq
-
-
70
@issue_statuses = IssueStatus.find_all_by_id(ids).sort
-
end
-
-
1
def disabled_core_fields
-
374
i = -1
-
1486
@disabled_core_fields ||= CORE_FIELDS.select { i += 1; (fields_bits || 0) & (2 ** i) != 0}
-
end
-
-
1
def core_fields
-
CORE_FIELDS - disabled_core_fields
-
end
-
-
1
def core_fields=(fields)
-
raise ArgumentError.new("Tracker.core_fields takes an array") unless fields.is_a?(Array)
-
-
bits = 0
-
CORE_FIELDS.each_with_index do |field, i|
-
unless fields.include?(field)
-
bits |= 2 ** i
-
end
-
end
-
self.fields_bits = bits
-
@disabled_core_fields = nil
-
core_fields
-
end
-
-
# Returns the fields that are disabled for all the given trackers
-
1
def self.disabled_core_fields(trackers)
-
51
if trackers.present?
-
51
trackers.uniq.map(&:disabled_core_fields).reduce(:&)
-
else
-
[]
-
end
-
end
-
-
# Returns the fields that are enabled for one tracker at least
-
1
def self.core_fields(trackers)
-
if trackers.present?
-
trackers.uniq.map(&:core_fields).reduce(:|)
-
else
-
CORE_FIELDS.dup
-
end
-
end
-
-
1
private
-
1
def check_integrity
-
raise Exception.new("Can't delete tracker") if Issue.where(:tracker_id => self.id).any?
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require "digest/sha1"
-
-
1
class User < Principal
-
1
include Redmine::SafeAttributes
-
-
# Account statuses
-
1
STATUS_ANONYMOUS = 0
-
1
STATUS_ACTIVE = 1
-
1
STATUS_REGISTERED = 2
-
1
STATUS_LOCKED = 3
-
-
# Different ways of displaying/sorting users
-
1
USER_FORMATS = {
-
:firstname_lastname => {
-
:string => '#{firstname} #{lastname}',
-
:order => %w(firstname lastname id),
-
:setting_order => 1
-
},
-
:firstname_lastinitial => {
-
:string => '#{firstname} #{lastname.to_s.chars.first}.',
-
:order => %w(firstname lastname id),
-
:setting_order => 2
-
},
-
:firstname => {
-
:string => '#{firstname}',
-
:order => %w(firstname id),
-
:setting_order => 3
-
},
-
:lastname_firstname => {
-
:string => '#{lastname} #{firstname}',
-
:order => %w(lastname firstname id),
-
:setting_order => 4
-
},
-
:lastname_coma_firstname => {
-
:string => '#{lastname}, #{firstname}',
-
:order => %w(lastname firstname id),
-
:setting_order => 5
-
},
-
:lastname => {
-
:string => '#{lastname}',
-
:order => %w(lastname id),
-
:setting_order => 6
-
},
-
:username => {
-
:string => '#{login}',
-
:order => %w(login id),
-
:setting_order => 7
-
},
-
}
-
-
1
MAIL_NOTIFICATION_OPTIONS = [
-
['all', :label_user_mail_option_all],
-
['selected', :label_user_mail_option_selected],
-
['only_my_events', :label_user_mail_option_only_my_events],
-
['only_assigned', :label_user_mail_option_only_assigned],
-
['only_owner', :label_user_mail_option_only_owner],
-
['none', :label_user_mail_option_none]
-
]
-
-
1
has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
-
:after_remove => Proc.new {|user, group| group.user_removed(user)}
-
1
has_many :changesets, :dependent => :nullify
-
1
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
-
1
has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
-
1
has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
-
1
belongs_to :auth_source
-
-
1
scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
-
1
scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
-
-
1
acts_as_customizable
-
-
1
attr_accessor :password, :password_confirmation
-
1
attr_accessor :last_before_login_on
-
# Prevents unauthorized assignments
-
1
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
-
-
1
LOGIN_LENGTH_LIMIT = 60
-
1
MAIL_LENGTH_LIMIT = 60
-
-
1
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
-
1
validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
-
1
validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
-
# Login must contain lettres, numbers, underscores only
-
1
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
-
1
validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
-
1
validates_length_of :firstname, :lastname, :maximum => 30
-
1
validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
-
1
validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
-
1
validates_confirmation_of :password, :allow_nil => true
-
1
validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
-
1
validate :validate_password_length
-
-
1
before_create :set_mail_notification
-
1
before_save :update_hashed_password
-
1
before_destroy :remove_references_before_destroy
-
-
1
scope :in_group, lambda {|group|
-
group_id = group.is_a?(Group) ? group.id : group.to_i
-
where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
-
}
-
1
scope :not_in_group, lambda {|group|
-
group_id = group.is_a?(Group) ? group.id : group.to_i
-
where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
-
}
-
-
1
def set_mail_notification
-
self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
-
true
-
end
-
-
1
def update_hashed_password
-
# update hashed_password if password was set
-
121
if self.password && self.auth_source_id.blank?
-
salt_password(password)
-
end
-
end
-
-
1
def reload(*args)
-
2
@name = nil
-
2
@projects_by_role = nil
-
2
super
-
end
-
-
1
def mail=(arg)
-
write_attribute(:mail, arg.to_s.strip)
-
end
-
-
1
def identity_url=(url)
-
if url.blank?
-
write_attribute(:identity_url, '')
-
else
-
begin
-
write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
-
rescue OpenIdAuthentication::InvalidOpenId
-
# Invlaid url, don't save
-
end
-
end
-
self.read_attribute(:identity_url)
-
end
-
-
# Returns the user that matches provided login and password, or nil
-
1
def self.try_to_login(login, password)
-
121
login = login.to_s
-
121
password = password.to_s
-
-
# Make sure no one can sign in with an empty password
-
121
return nil if password.empty?
-
121
user = find_by_login(login)
-
121
if user
-
# user is already in local database
-
121
return nil if !user.active?
-
121
if user.auth_source
-
# user has an external authentication method
-
return nil unless user.auth_source.authenticate(login, password)
-
else
-
# authentication with local password
-
121
return nil unless user.check_password?(password)
-
end
-
else
-
# user is not yet registered, try to authenticate with available sources
-
attrs = AuthSource.authenticate(login, password)
-
if attrs
-
user = new(attrs)
-
user.login = login
-
user.language = Setting.default_language
-
if user.save
-
user.reload
-
logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
-
end
-
end
-
end
-
121
user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
-
121
user
-
rescue => text
-
raise text
-
end
-
-
# Returns the user who matches the given autologin +key+ or nil
-
1
def self.try_to_autologin(key)
-
tokens = Token.find_all_by_action_and_value('autologin', key.to_s)
-
# Make sure there's only 1 token that matches the key
-
if tokens.size == 1
-
token = tokens.first
-
if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
-
token.user.update_attribute(:last_login_on, Time.now)
-
token.user
-
end
-
end
-
end
-
-
1
def self.name_formatter(formatter = nil)
-
5819
USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
-
end
-
-
# Returns an array of fields names than can be used to make an order statement for users
-
# according to how user names are displayed
-
# Examples:
-
#
-
# User.fields_for_order_statement => ['users.login', 'users.id']
-
# User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
-
1
def self.fields_for_order_statement(table=nil)
-
46
table ||= table_name
-
184
name_formatter[:order].map {|field| "#{table}.#{field}"}
-
end
-
-
# Return user's full name for display
-
1
def name(formatter = nil)
-
5773
f = self.class.name_formatter(formatter)
-
5773
if formatter
-
249
eval('"' + f[:string] + '"')
-
else
-
5524
@name ||= eval('"' + f[:string] + '"')
-
end
-
end
-
-
1
def active?
-
1628
self.status == STATUS_ACTIVE
-
end
-
-
1
def registered?
-
self.status == STATUS_REGISTERED
-
end
-
-
1
def locked?
-
self.status == STATUS_LOCKED
-
end
-
-
1
def activate
-
self.status = STATUS_ACTIVE
-
end
-
-
1
def register
-
self.status = STATUS_REGISTERED
-
end
-
-
1
def lock
-
self.status = STATUS_LOCKED
-
end
-
-
1
def activate!
-
update_attribute(:status, STATUS_ACTIVE)
-
end
-
-
1
def register!
-
update_attribute(:status, STATUS_REGISTERED)
-
end
-
-
1
def lock!
-
update_attribute(:status, STATUS_LOCKED)
-
end
-
-
# Returns true if +clear_password+ is the correct user's password, otherwise false
-
1
def check_password?(clear_password)
-
121
if auth_source_id.present?
-
auth_source.authenticate(self.login, clear_password)
-
else
-
121
User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
-
end
-
end
-
-
# Generates a random salt and computes hashed_password for +clear_password+
-
# The hashed password is stored in the following form: SHA1(salt + SHA1(password))
-
1
def salt_password(clear_password)
-
self.salt = User.generate_salt
-
self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
-
end
-
-
# Does the backend storage allow this user to change their password?
-
1
def change_password_allowed?
-
return true if auth_source.nil?
-
return auth_source.allow_password_changes?
-
end
-
-
# Generate and set a random password. Useful for automated user creation
-
# Based on Token#generate_token_value
-
#
-
1
def random_password
-
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
-
password = ''
-
40.times { |i| password << chars[rand(chars.size-1)] }
-
self.password = password
-
self.password_confirmation = password
-
self
-
end
-
-
1
def pref
-
2086
self.preference ||= UserPreference.new(:user => self)
-
end
-
-
1
def time_zone
-
327
@time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
-
end
-
-
1
def wants_comments_in_reverse_order?
-
6
self.pref[:comments_sorting] == 'desc'
-
end
-
-
# Return user's RSS key (a 40 chars long string), used to access feeds
-
1
def rss_key
-
428
if rss_token.nil?
-
118
create_rss_token(:action => 'feeds')
-
end
-
428
rss_token.value
-
end
-
-
# Return user's API key (a 40 chars long string), used to access the API
-
1
def api_key
-
19
if api_token.nil?
-
12
create_api_token(:action => 'api')
-
end
-
19
api_token.value
-
end
-
-
# Return an array of project ids for which the user has explicitly turned mail notifications on
-
1
def notified_projects_ids
-
@notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
-
end
-
-
1
def notified_project_ids=(ids)
-
Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
-
Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
-
@notified_projects_ids = nil
-
notified_projects_ids
-
end
-
-
1
def valid_notification_options
-
self.class.valid_notification_options(self)
-
end
-
-
# Only users that belong to more than 1 project can select projects for which they are notified
-
1
def self.valid_notification_options(user=nil)
-
# Note that @user.membership.size would fail since AR ignores
-
# :include association option when doing a count
-
if user.nil? || user.memberships.length < 1
-
MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
-
else
-
MAIL_NOTIFICATION_OPTIONS
-
end
-
end
-
-
# Find a user account by matching the exact login and then a case-insensitive
-
# version. Exact matches will be given priority.
-
1
def self.find_by_login(login)
-
# First look for an exact match
-
242
user = where(:login => login).all.detect {|u| u.login == login}
-
121
unless user
-
# Fail over to case-insensitive if none was found
-
user = where("LOWER(login) = ?", login.to_s.downcase).first
-
end
-
121
user
-
end
-
-
1
def self.find_by_rss_key(key)
-
token = Token.find_by_action_and_value('feeds', key.to_s)
-
token && token.user.active? ? token.user : nil
-
end
-
-
1
def self.find_by_api_key(key)
-
2
token = Token.find_by_action_and_value('api', key.to_s)
-
2
token && token.user.active? ? token.user : nil
-
end
-
-
# Makes find_by_mail case-insensitive
-
1
def self.find_by_mail(mail)
-
where("LOWER(mail) = ?", mail.to_s.downcase).first
-
end
-
-
# Returns true if the default admin account can no longer be used
-
1
def self.default_admin_account_changed?
-
!User.active.find_by_login("admin").try(:check_password?, "admin")
-
end
-
-
1
def to_s
-
4779
name
-
end
-
-
1
CSS_CLASS_BY_STATUS = {
-
STATUS_ANONYMOUS => 'anon',
-
STATUS_ACTIVE => 'active',
-
STATUS_REGISTERED => 'registered',
-
STATUS_LOCKED => 'locked'
-
}
-
-
1
def css_classes
-
503
"user #{CSS_CLASS_BY_STATUS[status]}"
-
end
-
-
# Returns the current day according to user's time zone
-
1
def today
-
4
if time_zone.nil?
-
4
Date.today
-
else
-
Time.now.in_time_zone(time_zone).to_date
-
end
-
end
-
-
# Returns the day of +time+ according to user's time zone
-
1
def time_to_date(time)
-
106
if time_zone.nil?
-
106
time.to_date
-
else
-
time.in_time_zone(time_zone).to_date
-
end
-
end
-
-
1
def logged?
-
25570
true
-
end
-
-
1
def anonymous?
-
19
!logged?
-
end
-
-
# Return user's roles for project
-
1
def roles_for_project(project)
-
8998
roles = []
-
# No role on archived projects
-
8998
return roles if project.nil? || project.archived?
-
8984
if logged?
-
# Find project membership
-
35633
membership = memberships.detect {|m| m.project_id == project.id}
-
8978
if membership
-
8889
roles = membership.roles
-
else
-
89
@role_non_member ||= Role.non_member
-
89
roles << @role_non_member
-
end
-
else
-
6
@role_anonymous ||= Role.anonymous
-
6
roles << @role_anonymous
-
end
-
8984
roles
-
end
-
-
# Return true if the user is a member of project
-
1
def member_of?(project)
-
!roles_for_project(project).detect {|role| role.member?}.nil?
-
end
-
-
# Returns a hash of user's projects grouped by roles
-
1
def projects_by_role
-
1597
return @projects_by_role if @projects_by_role
-
-
368
@projects_by_role = Hash.new([])
-
368
memberships.each do |membership|
-
1329
if membership.project
-
1329
membership.roles.each do |role|
-
1329
@projects_by_role[role] = [] unless @projects_by_role.key?(role)
-
1329
@projects_by_role[role] << membership.project
-
end
-
end
-
end
-
368
@projects_by_role.each do |role, projects|
-
736
projects.uniq!
-
end
-
-
368
@projects_by_role
-
end
-
-
# Returns true if user is arg or belongs to arg
-
1
def is_or_belongs_to?(arg)
-
if arg.is_a?(User)
-
self == arg
-
elsif arg.is_a?(Group)
-
arg.users.include?(self)
-
else
-
false
-
end
-
end
-
-
# Return true if the user is allowed to do the specified action on a specific context
-
# Action can be:
-
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
-
# * a permission Symbol (eg. :edit_project)
-
# Context can be:
-
# * a project : returns true if user is allowed to do the specified action on this project
-
# * an array of projects : returns true if user is allowed on every project
-
# * nil with options[:global] set : check if user has at least one role allowed for this action,
-
# or falls back to Non Member / Anonymous permissions depending if the user is logged
-
1
def allowed_to?(action, context, options={}, &block)
-
7858
if context && context.is_a?(Project)
-
7167
return false unless context.allows_to?(action)
-
# Admin users are authorized for anything else
-
7146
return true if admin?
-
-
7140
roles = roles_for_project(context)
-
7140
return false unless roles
-
7140
roles.any? {|role|
-
7140
(context.is_public? || role.member?) &&
-
7140
role.allowed_to?(action) &&
-
6621
(block_given? ? yield(role, self) : true)
-
}
-
691
elsif context && context.is_a?(Array)
-
if context.empty?
-
false
-
else
-
# Authorize if user is authorized on every element of the array
-
context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
-
end
-
691
elsif options[:global]
-
# Admin users are always authorized
-
691
return true if admin?
-
-
# authorize if user has at least one role that has this permission
-
2025
roles = memberships.collect {|m| m.roles}.flatten.uniq
-
681
roles << (self.logged? ? Role.non_member : Role.anonymous)
-
681
roles.any? {|role|
-
role.allowed_to?(action) &&
-
1346
(block_given? ? yield(role, self) : true)
-
}
-
else
-
false
-
end
-
end
-
-
# Is the user allowed to do the specified action on any project?
-
# See allowed_to? for the actions and valid options.
-
1
def allowed_to_globally?(action, options, &block)
-
allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
-
end
-
-
# Returns true if the user is allowed to delete his own account
-
1
def own_account_deletable?
-
Setting.unsubscribe? &&
-
(!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
-
end
-
-
1
safe_attributes 'login',
-
'firstname',
-
'lastname',
-
'mail',
-
'mail_notification',
-
'language',
-
'custom_field_values',
-
'custom_fields',
-
'identity_url'
-
-
1
safe_attributes 'status',
-
'auth_source_id',
-
:if => lambda {|user, current_user| current_user.admin?}
-
-
1
safe_attributes 'group_ids',
-
:if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
-
-
# Utility method to help check if a user should be notified about an
-
# event.
-
#
-
# TODO: only supports Issue events currently
-
1
def notify_about?(object)
-
1003
case mail_notification
-
when 'all'
-
1003
true
-
when 'selected'
-
# user receives notifications for created/assigned issues on unselected projects
-
if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
-
true
-
else
-
false
-
end
-
when 'none'
-
false
-
when 'only_my_events'
-
if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
-
true
-
else
-
false
-
end
-
when 'only_assigned'
-
if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
-
true
-
else
-
false
-
end
-
when 'only_owner'
-
if object.is_a?(Issue) && object.author == self
-
true
-
else
-
false
-
end
-
else
-
false
-
end
-
end
-
-
1
def self.current=(user)
-
983
@current_user = user
-
end
-
-
1
def self.current
-
30291
@current_user ||= User.anonymous
-
end
-
-
# Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
-
# one anonymous user per database.
-
1
def self.anonymous
-
279
anonymous_user = AnonymousUser.first
-
279
if anonymous_user.nil?
-
anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
-
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
-
end
-
279
anonymous_user
-
end
-
-
# Salts all existing unsalted passwords
-
# It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
-
# This method is used in the SaltPasswords migration and is to be kept as is
-
1
def self.salt_unsalted_passwords!
-
transaction do
-
User.where("salt IS NULL OR salt = ''").find_each do |user|
-
next if user.hashed_password.blank?
-
salt = User.generate_salt
-
hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
-
User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
-
end
-
end
-
end
-
-
1
protected
-
-
1
def validate_password_length
-
# Password length validation based on setting
-
if !password.nil? && password.size < Setting.password_min_length.to_i
-
errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
-
end
-
end
-
-
1
private
-
-
# Removes references that are not handled by associations
-
# Things that are not deleted are reassociated with the anonymous user
-
1
def remove_references_before_destroy
-
return if self.id.nil?
-
-
substitute = User.anonymous
-
Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
-
Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
-
JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
-
JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
-
Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
# Remove private queries and keep public ones
-
::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
-
::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
-
TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
-
Token.delete_all ['user_id = ?', id]
-
Watcher.delete_all ['user_id = ?', id]
-
WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
end
-
-
# Return password digest
-
1
def self.hash_password(clear_password)
-
242
Digest::SHA1.hexdigest(clear_password || "")
-
end
-
-
# Returns a 128bits random salt as a hex string (32 chars long)
-
1
def self.generate_salt
-
Redmine::Utils.random_hex(16)
-
end
-
-
end
-
-
1
class AnonymousUser < User
-
1
validate :validate_anonymous_uniqueness, :on => :create
-
-
1
def validate_anonymous_uniqueness
-
# There should be only one AnonymousUser in the database
-
errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
-
end
-
-
1
def available_custom_fields
-
[]
-
end
-
-
# Overrides a few properties
-
2063
def logged?; false end
-
1
def admin; false end
-
1
def name(*args); I18n.t(:label_user_anonymous) end
-
1
def mail; nil end
-
1
def time_zone; nil end
-
1
def rss_key; nil end
-
-
1
def pref
-
125
UserPreference.new(:user => self)
-
end
-
-
# Anonymous user can not be destroyed
-
1
def destroy
-
false
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class UserCustomField < CustomField
-
1
def type_name
-
:label_user_plural
-
end
-
end
-
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class UserPreference < ActiveRecord::Base
-
1
belongs_to :user
-
1
serialize :others
-
-
1
attr_protected :others, :user_id
-
-
1
before_save :set_others_hash
-
-
1
def initialize(attributes=nil, *args)
-
242
super
-
242
self.others ||= {}
-
end
-
-
1
def set_others_hash
-
193
self.others ||= {}
-
end
-
-
1
def [](attr_name)
-
2319
if attribute_present? attr_name
-
359
super
-
else
-
1960
others ? others[attr_name] : nil
-
end
-
end
-
-
1
def []=(attr_name, value)
-
552
if attribute_present? attr_name
-
476
super
-
else
-
76
h = (read_attribute(:others) || {}).dup
-
76
h.update(attr_name => value)
-
76
write_attribute(:others, h)
-
76
value
-
end
-
end
-
-
1
def comments_sorting; self[:comments_sorting] end
-
1
def comments_sorting=(order); self[:comments_sorting]=order end
-
-
375
def warn_on_leaving_unsaved; self[:warn_on_leaving_unsaved] || '1'; end
-
1
def warn_on_leaving_unsaved=(value); self[:warn_on_leaving_unsaved]=value; end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Version < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
after_update :update_issues_from_sharing_change
-
1
belongs_to :project
-
1
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
-
1
acts_as_customizable
-
acts_as_attachable :view_permission => :view_files,
-
1
:delete_permission => :manage_files
-
-
1
VERSION_STATUSES = %w(open locked closed)
-
1
VERSION_SHARINGS = %w(none descendants hierarchy tree system)
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name, :scope => [:project_id]
-
1
validates_length_of :name, :maximum => 60
-
1
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
-
1
validates_inclusion_of :status, :in => VERSION_STATUSES
-
1
validates_inclusion_of :sharing, :in => VERSION_SHARINGS
-
1
validate :validate_version
-
-
1
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
-
1
scope :open, where(:status => 'open')
-
1
scope :visible, lambda {|*args|
-
61
includes(:project).where(Project.allowed_to_condition(args.first || User.current, :view_issues))
-
}
-
-
1
safe_attributes 'name',
-
'description',
-
'effective_date',
-
'due_date',
-
'wiki_page_title',
-
'status',
-
'sharing',
-
'custom_field_values'
-
-
# Returns true if +user+ or current user is allowed to view the version
-
1
def visible?(user=User.current)
-
58
user.allowed_to?(:view_issues, self.project)
-
end
-
-
# Version files have same visibility as project files
-
1
def attachments_visible?(*args)
-
project.present? && project.attachments_visible?(*args)
-
end
-
-
1
def start_date
-
@start_date ||= fixed_issues.minimum('start_date')
-
end
-
-
1
def due_date
-
effective_date
-
end
-
-
1
def due_date=(arg)
-
self.effective_date=(arg)
-
end
-
-
# Returns the total estimated time for this version
-
# (sum of leaves estimated_hours)
-
1
def estimated_hours
-
2
@estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
-
end
-
-
# Returns the total reported time for this version
-
1
def spent_hours
-
5
@spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f
-
end
-
-
1
def closed?
-
12
status == 'closed'
-
end
-
-
1
def open?
-
24
status == 'open'
-
end
-
-
# Returns true if the version is completed: due date reached and no open issues
-
1
def completed?
-
13
effective_date && (effective_date < Date.today) && (open_issues_count == 0)
-
end
-
-
1
def behind_schedule?
-
if completed_pourcent == 100
-
return false
-
elsif due_date && start_date
-
done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
-
return done_date <= Date.today
-
else
-
false # No issues so it's not late
-
end
-
end
-
-
# Returns the completion percentage of this version based on the amount of open/closed issues
-
# and the time spent on the open issues.
-
1
def completed_pourcent
-
2
if issues_count == 0
-
0
-
2
elsif open_issues_count == 0
-
100
-
else
-
2
issues_progress(false) + issues_progress(true)
-
end
-
end
-
-
# Returns the percentage of issues that have been marked as 'closed'.
-
1
def closed_pourcent
-
1
if issues_count == 0
-
0
-
else
-
1
issues_progress(false)
-
end
-
end
-
-
# Returns true if the version is overdue: due date reached and some open issues
-
1
def overdue?
-
effective_date && (effective_date < Date.today) && (open_issues_count > 0)
-
end
-
-
# Returns assigned issues count
-
1
def issues_count
-
9
load_issue_counts
-
9
@issue_count
-
end
-
-
# Returns the total amount of open issues for this version.
-
1
def open_issues_count
-
17
load_issue_counts
-
17
@open_issues_count
-
end
-
-
# Returns the total amount of closed issues for this version.
-
1
def closed_issues_count
-
2
load_issue_counts
-
2
@closed_issues_count
-
end
-
-
1
def wiki_page
-
2
if project.wiki && !wiki_page_title.blank?
-
2
@wiki_page ||= project.wiki.find_page(wiki_page_title)
-
end
-
2
@wiki_page
-
end
-
-
1439
def to_s; name end
-
-
1
def to_s_with_project
-
"#{project} - #{name}"
-
end
-
-
# Versions are sorted by effective_date and name
-
# Those with no effective_date are at the end, sorted by name
-
1
def <=>(version)
-
3831
if self.effective_date
-
2518
if version.effective_date
-
2381
if self.effective_date == version.effective_date
-
758
name == version.name ? id <=> version.id : name <=> version.name
-
else
-
1623
self.effective_date <=> version.effective_date
-
end
-
else
-
137
-1
-
end
-
else
-
1313
if version.effective_date
-
674
1
-
else
-
639
name == version.name ? id <=> version.id : name <=> version.name
-
end
-
end
-
end
-
-
1
def self.fields_for_order_statement(table=nil)
-
17
table ||= table_name
-
17
["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
-
end
-
-
1
scope :sorted, order(fields_for_order_statement)
-
-
# Returns the sharings that +user+ can set the version to
-
1
def allowed_sharings(user = User.current)
-
1
VERSION_SHARINGS.select do |s|
-
5
if sharing == s
-
1
true
-
else
-
4
case s
-
when 'system'
-
# Only admin users can set a systemwide sharing
-
1
user.admin?
-
when 'hierarchy', 'tree'
-
# Only users allowed to manage versions of the root project can
-
# set sharing to hierarchy or tree
-
2
project.nil? || user.allowed_to?(:manage_versions, project.root)
-
else
-
1
true
-
end
-
end
-
end
-
end
-
-
1
private
-
-
1
def load_issue_counts
-
28
unless @issue_count
-
13
@open_issues_count = 0
-
13
@closed_issues_count = 0
-
13
fixed_issues.count(:all, :group => :status).each do |status, count|
-
13
if status.is_closed?
-
@closed_issues_count += count
-
else
-
13
@open_issues_count += count
-
end
-
end
-
13
@issue_count = @open_issues_count + @closed_issues_count
-
end
-
end
-
-
# Update the issue's fixed versions. Used if a version's sharing changes.
-
1
def update_issues_from_sharing_change
-
17
if sharing_changed?
-
if VERSION_SHARINGS.index(sharing_was).nil? ||
-
VERSION_SHARINGS.index(sharing).nil? ||
-
VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
-
Issue.update_versions_from_sharing_change self
-
end
-
end
-
end
-
-
# Returns the average estimated time of assigned issues
-
# or 1 if no issue has an estimated time
-
# Used to weigth unestimated issues in progress calculation
-
1
def estimated_average
-
4
if @estimated_average.nil?
-
1
average = fixed_issues.average(:estimated_hours).to_f
-
1
if average == 0
-
1
average = 1
-
end
-
1
@estimated_average = average
-
end
-
4
@estimated_average
-
end
-
-
# Returns the total progress of open or closed issues. The returned percentage takes into account
-
# the amount of estimated time set for this version.
-
#
-
# Examples:
-
# issues_progress(true) => returns the progress percentage for open issues.
-
# issues_progress(false) => returns the progress percentage for closed issues.
-
1
def issues_progress(open)
-
5
@issues_progress ||= {}
-
5
@issues_progress[open] ||= begin
-
2
progress = 0
-
2
if issues_count > 0
-
2
ratio = open ? 'done_ratio' : 100
-
-
2
done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f
-
2
progress = done / (estimated_average * issues_count)
-
end
-
2
progress
-
end
-
end
-
-
1
def validate_version
-
393
if effective_date.nil? && @attributes['effective_date'].present?
-
errors.add :effective_date, :not_a_date
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class VersionCustomField < CustomField
-
1
def type_name
-
:label_version_plural
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Watcher < ActiveRecord::Base
-
1
belongs_to :watchable, :polymorphic => true
-
1
belongs_to :user
-
-
1
validates_presence_of :user
-
1
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
-
1
validate :validate_user
-
-
# Unwatch things that users are no longer allowed to view
-
1
def self.prune(options={})
-
if options.has_key?(:user)
-
prune_single_user(options[:user], options)
-
else
-
pruned = 0
-
User.find(:all, :conditions => "id IN (SELECT DISTINCT user_id FROM #{table_name})").each do |user|
-
pruned += prune_single_user(user, options)
-
end
-
pruned
-
end
-
end
-
-
1
protected
-
-
1
def validate_user
-
errors.add :user_id, :invalid unless user.nil? || user.active?
-
end
-
-
1
private
-
-
1
def self.prune_single_user(user, options={})
-
return unless user.is_a?(User)
-
pruned = 0
-
find(:all, :conditions => {:user_id => user.id}).each do |watcher|
-
next if watcher.watchable.nil?
-
-
if options.has_key?(:project)
-
next unless watcher.watchable.respond_to?(:project) && watcher.watchable.project == options[:project]
-
end
-
-
if watcher.watchable.respond_to?(:visible?)
-
unless watcher.watchable.visible?(user)
-
watcher.destroy
-
pruned += 1
-
end
-
end
-
end
-
pruned
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Wiki < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :project
-
1
has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
-
1
has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
-
-
1
acts_as_watchable
-
-
1
validates_presence_of :start_page
-
1
validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
-
-
1
safe_attributes 'start_page'
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_wiki_pages, project)
-
end
-
-
# Returns the wiki page that acts as the sidebar content
-
# or nil if no such page exists
-
1
def sidebar
-
2
@sidebar ||= find_page('Sidebar', :with_redirect => false)
-
end
-
-
# find the page with the given title
-
# if page doesn't exist, return a new page
-
1
def find_or_new_page(title)
-
4
title = start_page if title.blank?
-
4
find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
-
end
-
-
# find the page with the given title
-
1
def find_page(title, options = {})
-
19
@page_found_with_redirect = false
-
19
title = start_page if title.blank?
-
19
title = Wiki.titleize(title)
-
19
page = pages.first(:conditions => ["LOWER(title) = LOWER(?)", title])
-
19
if !page && !(options[:with_redirect] == false)
-
# search for a redirect
-
6
redirect = redirects.first(:conditions => ["LOWER(title) = LOWER(?)", title])
-
6
if redirect
-
page = find_page(redirect.redirects_to, :with_redirect => false)
-
@page_found_with_redirect = true
-
end
-
end
-
19
page
-
end
-
-
# Returns true if the last page was found with a redirect
-
1
def page_found_with_redirect?
-
4
@page_found_with_redirect
-
end
-
-
# Finds a page by title
-
# The given string can be of one of the forms: "title" or "project:title"
-
# Examples:
-
# Wiki.find_page("bar", project => foo)
-
# Wiki.find_page("foo:bar")
-
1
def self.find_page(title, options = {})
-
project = options[:project]
-
if title.to_s =~ %r{^([^\:]+)\:(.*)$}
-
project_identifier, title = $1, $2
-
project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
-
end
-
if project && project.wiki
-
page = project.wiki.find_page(title)
-
if page && page.content
-
page
-
end
-
end
-
end
-
-
# turn a string into a valid page title
-
1
def self.titleize(title)
-
# replace spaces with _ and remove unwanted caracters
-
54
title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
-
# upcase the first letter
-
54
title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
-
54
title
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'zlib'
-
-
1
class WikiContent < ActiveRecord::Base
-
1
self.locking_column = 'version'
-
1
belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
-
1
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
-
1
validates_presence_of :text
-
1
validates_length_of :comments, :maximum => 255, :allow_nil => true
-
-
1
acts_as_versioned
-
-
1
def visible?(user=User.current)
-
page.visible?(user)
-
end
-
-
1
def project
-
page.project
-
end
-
-
1
def attachments
-
3
page.nil? ? [] : page.attachments
-
end
-
-
# Returns the mail adresses of users that should be notified
-
1
def recipients
-
notified = project.notified_users
-
notified.reject! {|user| !visible?(user)}
-
notified.collect(&:mail)
-
end
-
-
# Return true if the content is the current page content
-
1
def current_version?
-
6
true
-
end
-
-
1
class Version
-
1
belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
-
1
belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
-
1
attr_protected :data
-
-
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
-
:description => :comments,
-
:datetime => :updated_on,
-
:type => 'wiki-page',
-
1
:url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.page.wiki.project, :id => o.page.title, :version => o.version}}
-
-
acts_as_activity_provider :type => 'wiki_edits',
-
:timestamp => "#{WikiContent.versioned_table_name}.updated_on",
-
:author_key => "#{WikiContent.versioned_table_name}.author_id",
-
:permission => :view_wiki_edits,
-
:find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
-
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
-
"#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
-
"#{WikiContent.versioned_table_name}.id",
-
:joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
-
"LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
-
1
"LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"}
-
-
1
after_destroy :page_update_after_destroy
-
-
1
def text=(plain)
-
6
case Setting.wiki_compression
-
when 'gzip'
-
begin
-
self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
-
self.compression = 'gzip'
-
rescue
-
self.data = plain
-
self.compression = ''
-
end
-
else
-
6
self.data = plain
-
6
self.compression = ''
-
end
-
6
plain
-
end
-
-
1
def text
-
@text ||= begin
-
str = case compression
-
when 'gzip'
-
Zlib::Inflate.inflate(data)
-
else
-
# uncompressed data
-
data
-
end
-
str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
-
str
-
end
-
end
-
-
1
def project
-
page.project
-
end
-
-
# Return true if the content is the current page content
-
1
def current_version?
-
page.content.version == self.version
-
end
-
-
# Returns the previous version or nil
-
1
def previous
-
@previous ||= WikiContent::Version.
-
reorder('version DESC').
-
includes(:author).
-
where("wiki_content_id = ? AND version < ?", wiki_content_id, version).first
-
end
-
-
# Returns the next version or nil
-
1
def next
-
@next ||= WikiContent::Version.
-
reorder('version ASC').
-
includes(:author).
-
where("wiki_content_id = ? AND version > ?", wiki_content_id, version).first
-
end
-
-
1
private
-
-
# Updates page's content if the latest version is removed
-
# or destroys the page if it was the only version
-
1
def page_update_after_destroy
-
latest = page.content.versions.reorder("#{self.class.table_name}.version DESC").first
-
if latest && page.content.version != latest.version
-
raise ActiveRecord::Rollback unless page.content.revert_to!(latest)
-
elsif latest.nil?
-
raise ActiveRecord::Rollback unless page.destroy
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WikiContentObserver < ActiveRecord::Observer
-
1
def after_create(wiki_content)
-
6
Mailer.wiki_content_added(wiki_content).deliver if Setting.notified_events.include?('wiki_content_added')
-
end
-
-
1
def after_update(wiki_content)
-
if wiki_content.text_changed?
-
Mailer.wiki_content_updated(wiki_content).deliver if Setting.notified_events.include?('wiki_content_updated')
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'diff'
-
1
require 'enumerator'
-
-
1
class WikiPage < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
-
1
belongs_to :wiki
-
1
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
-
1
acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
-
1
acts_as_tree :dependent => :nullify, :order => 'title'
-
-
1
acts_as_watchable
-
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
-
:description => :text,
-
:datetime => :created_on,
-
1
:url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
-
-
acts_as_searchable :columns => ['title', "#{WikiContent.table_name}.text"],
-
:include => [{:wiki => :project}, :content],
-
:permission => :view_wiki_pages,
-
1
:project_key => "#{Wiki.table_name}.project_id"
-
-
1
attr_accessor :redirect_existing_links
-
-
1
validates_presence_of :title
-
1
validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
-
1
validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
-
1
validates_associated :content
-
-
1
validate :validate_parent_title
-
1
before_destroy :remove_redirects
-
1
before_save :handle_redirects
-
-
# eager load information about last updates, without loading text
-
1
scope :with_updated_on, {
-
:select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on, #{WikiContent.table_name}.version",
-
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id"
-
}
-
-
# Wiki pages that are protected by default
-
1
DEFAULT_PROTECTED_PAGES = %w(sidebar)
-
-
1
safe_attributes 'parent_id', 'parent_title',
-
2
:if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)}
-
-
1
def initialize(attributes=nil, *args)
-
6
super
-
6
if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
-
self.protected = true
-
end
-
end
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_wiki_pages, project)
-
end
-
-
1
def title=(value)
-
12
value = Wiki.titleize(value)
-
12
@previous_title = read_attribute(:title) if @previous_title.blank?
-
12
write_attribute(:title, value)
-
end
-
-
1
def handle_redirects
-
6
self.title = Wiki.titleize(title)
-
# Manage redirects if the title has changed
-
6
if !@previous_title.blank? && (@previous_title != title) && !new_record?
-
# Update redirects that point to the old title
-
wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
-
r.redirects_to = title
-
r.title == r.redirects_to ? r.destroy : r.save
-
end
-
# Remove redirects for the new title
-
wiki.redirects.find_all_by_title(title).each(&:destroy)
-
# Create a redirect to the new title
-
wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
-
@previous_title = nil
-
end
-
end
-
-
1
def remove_redirects
-
# Remove redirects to this page
-
wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
-
end
-
-
1
def pretty_title
-
24
WikiPage.pretty_title(title)
-
end
-
-
1
def content_for_version(version=nil)
-
4
result = content.versions.find_by_version(version.to_i) if version
-
4
result ||= content
-
4
result
-
end
-
-
1
def diff(version_to=nil, version_from=nil)
-
version_to = version_to ? version_to.to_i : self.content.version
-
content_to = content.versions.find_by_version(version_to)
-
content_from = version_from ? content.versions.find_by_version(version_from.to_i) : content_to.try(:previous)
-
return nil unless content_to && content_from
-
-
if content_from.version > content_to.version
-
content_to, content_from = content_from, content_to
-
end
-
-
(content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
-
end
-
-
1
def annotate(version=nil)
-
version = version ? version.to_i : self.content.version
-
c = content.versions.find_by_version(version)
-
c ? WikiAnnotate.new(c) : nil
-
end
-
-
1
def self.pretty_title(str)
-
24
(str && str.is_a?(String)) ? str.tr('_', ' ') : str
-
end
-
-
1
def project
-
6
wiki.project
-
end
-
-
1
def text
-
3
content.text if content
-
end
-
-
1
def updated_on
-
unless @updated_on
-
if time = read_attribute(:updated_on)
-
# content updated_on was eager loaded with the page
-
begin
-
@updated_on = (self.class.default_timezone == :utc ? Time.parse(time.to_s).utc : Time.parse(time.to_s).localtime)
-
rescue
-
end
-
else
-
@updated_on = content && content.updated_on
-
end
-
end
-
@updated_on
-
end
-
-
# Returns true if usr is allowed to edit the page, otherwise false
-
1
def editable_by?(usr)
-
4
!protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
-
end
-
-
1
def attachments_deletable?(usr=User.current)
-
editable_by?(usr) && super(usr)
-
end
-
-
1
def parent_title
-
@parent_title || (self.parent && self.parent.pretty_title)
-
end
-
-
1
def parent_title=(t)
-
@parent_title = t
-
parent_page = t.blank? ? nil : self.wiki.find_page(t)
-
self.parent = parent_page
-
end
-
-
# Saves the page and its content if text was changed
-
1
def save_with_content
-
ret = nil
-
transaction do
-
if new_record?
-
# Rails automatically saves associated content
-
ret = save
-
else
-
ret = save && (content.text_changed? ? content.save : true)
-
end
-
raise ActiveRecord::Rollback unless ret
-
end
-
ret
-
end
-
-
1
protected
-
-
1
def validate_parent_title
-
9
errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
-
9
errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
-
9
errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
-
end
-
end
-
-
1
class WikiDiff < Redmine::Helpers::Diff
-
1
attr_reader :content_to, :content_from
-
-
1
def initialize(content_to, content_from)
-
@content_to = content_to
-
@content_from = content_from
-
super(content_to.text, content_from.text)
-
end
-
end
-
-
1
class WikiAnnotate
-
1
attr_reader :lines, :content
-
-
1
def initialize(content)
-
@content = content
-
current = content
-
current_lines = current.text.split(/\r?\n/)
-
@lines = current_lines.collect {|t| [nil, nil, t]}
-
positions = []
-
current_lines.size.times {|i| positions << i}
-
while (current.previous)
-
d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
-
d.each_slice(3) do |s|
-
sign, line = s[0], s[1]
-
if sign == '+' && positions[line] && positions[line] != -1
-
if @lines[positions[line]][0].nil?
-
@lines[positions[line]][0] = current.version
-
@lines[positions[line]][1] = current.author
-
end
-
end
-
end
-
d.each_slice(3) do |s|
-
sign, line = s[0], s[1]
-
if sign == '-'
-
positions.insert(line, -1)
-
else
-
positions[line] = nil
-
end
-
end
-
positions.compact!
-
# Stop if every line is annotated
-
break unless @lines.detect { |line| line[0].nil? }
-
current = current.previous
-
end
-
@lines.each { |line|
-
line[0] ||= current.version
-
# if the last known version is > 1 (eg. history was cleared), we don't know the author
-
line[1] ||= current.author if current.version == 1
-
}
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WikiRedirect < ActiveRecord::Base
-
1
belongs_to :wiki
-
-
1
validates_presence_of :title, :redirects_to
-
1
validates_length_of :title, :redirects_to, :maximum => 255
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WorkflowPermission < WorkflowRule
-
1
validates_inclusion_of :rule, :in => %w(readonly required)
-
1
validate :validate_field_name
-
-
# Replaces the workflow permissions for the given tracker and role
-
#
-
# Example:
-
# WorkflowPermission.replace_permissions role, tracker, {'due_date' => {'1' => 'readonly', '2' => 'required'}}
-
1
def self.replace_permissions(tracker, role, permissions)
-
destroy_all(:tracker_id => tracker.id, :role_id => role.id)
-
-
permissions.each { |field, rule_by_status_id|
-
rule_by_status_id.each { |status_id, rule|
-
if rule.present?
-
WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule)
-
end
-
}
-
}
-
end
-
-
1
protected
-
-
1
def validate_field_name
-
unless Tracker::CORE_FIELDS_ALL.include?(field_name) || field_name.to_s.match(/^\d+$/)
-
errors.add :field_name, :invalid
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WorkflowRule < ActiveRecord::Base
-
1
self.table_name = "#{table_name_prefix}workflows#{table_name_suffix}"
-
-
1
belongs_to :role
-
1
belongs_to :tracker
-
1
belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id'
-
1
belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id'
-
-
1
validates_presence_of :role, :tracker, :old_status
-
-
# Copies workflows from source to targets
-
1
def self.copy(source_tracker, source_role, target_trackers, target_roles)
-
376
unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role)
-
raise ArgumentError.new("source_tracker or source_role must be specified")
-
end
-
-
376
target_trackers = [target_trackers].flatten.compact
-
376
target_roles = [target_roles].flatten.compact
-
-
376
target_trackers = Tracker.sorted.all if target_trackers.empty?
-
376
target_roles = Role.all if target_roles.empty?
-
-
376
target_trackers.each do |target_tracker|
-
376
target_roles.each do |target_role|
-
1880
copy_one(source_tracker || target_tracker,
-
source_role || target_role,
-
target_tracker,
-
target_role)
-
end
-
end
-
end
-
-
# Copies a single set of workflows from source to target
-
1
def self.copy_one(source_tracker, source_role, target_tracker, target_role)
-
1880
unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? &&
-
source_role.is_a?(Role) && !source_role.new_record? &&
-
target_tracker.is_a?(Tracker) && !target_tracker.new_record? &&
-
target_role.is_a?(Role) && !target_role.new_record?
-
-
raise ArgumentError.new("arguments can not be nil or unsaved objects")
-
end
-
-
1880
if source_tracker == target_tracker && source_role == target_role
-
false
-
else
-
1880
transaction do
-
1880
delete_all :tracker_id => target_tracker.id, :role_id => target_role.id
-
1880
connection.insert "INSERT INTO #{WorkflowRule.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee, field_name, rule, type)" +
-
" SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee, field_name, rule, type" +
-
" FROM #{WorkflowRule.table_name}" +
-
" WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}"
-
end
-
1880
true
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WorkflowTransition < WorkflowRule
-
1
validates_presence_of :new_status
-
-
# Returns workflow transitions count by tracker and role
-
1
def self.count_by_tracker_and_role
-
counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{table_name} WHERE type = 'WorkflowTransition' GROUP BY role_id, tracker_id")
-
roles = Role.sorted.all
-
trackers = Tracker.sorted.all
-
-
result = []
-
trackers.each do |tracker|
-
t = []
-
roles.each do |role|
-
row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s}
-
t << [role, (row.nil? ? 0 : row['c'].to_i)]
-
end
-
result << [tracker, t]
-
end
-
-
result
-
end
-
end
-
1
require 'rexml/document'
-
1
require 'SVG/Graph/Graph'
-
1
require 'SVG/Graph/BarBase'
-
-
1
module SVG
-
1
module Graph
-
# === Create presentation quality SVG bar graphs easily
-
#
-
# = Synopsis
-
#
-
# require 'SVG/Graph/Bar'
-
#
-
# fields = %w(Jan Feb Mar);
-
# data_sales_02 = [12, 45, 21]
-
#
-
# graph = SVG::Graph::Bar.new(
-
# :height => 500,
-
# :width => 300,
-
# :fields => fields
-
# )
-
#
-
# graph.add_data(
-
# :data => data_sales_02,
-
# :title => 'Sales 2002'
-
# )
-
#
-
# print "Content-type: image/svg+xml\r\n\r\n"
-
# print graph.burn
-
#
-
# = Description
-
#
-
# This object aims to allow you to easily create high quality
-
# SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default
-
# style sheet or supply your own. Either way there are many options which
-
# can be configured to give you control over how the graph is generated -
-
# with or without a key, data elements at each point, title, subtitle etc.
-
#
-
# = Notes
-
#
-
# The default stylesheet handles upto 12 data sets, if you
-
# use more you must create your own stylesheet and add the
-
# additional settings for the extra data sets. You will know
-
# if you go over 12 data sets as they will have no style and
-
# be in black.
-
#
-
# = Examples
-
#
-
# * http://germane-software.com/repositories/public/SVG/test/test.rb
-
#
-
# = See also
-
#
-
# * SVG::Graph::Graph
-
# * SVG::Graph::BarHorizontal
-
# * SVG::Graph::Line
-
# * SVG::Graph::Pie
-
# * SVG::Graph::Plot
-
# * SVG::Graph::TimeSeries
-
1
class Bar < BarBase
-
1
include REXML
-
-
# See Graph::initialize and BarBase::set_defaults
-
1
def set_defaults
-
super
-
self.top_align = self.top_font = 1
-
end
-
-
1
protected
-
-
1
def get_x_labels
-
@config[:fields]
-
end
-
-
1
def get_y_labels
-
maxvalue = max_value
-
minvalue = min_value
-
range = maxvalue - minvalue
-
-
top_pad = range == 0 ? 10 : range / 20.0
-
scale_range = (maxvalue + top_pad) - minvalue
-
-
scale_division = scale_divisions || (scale_range / 10.0)
-
-
if scale_integers
-
scale_division = scale_division < 1 ? 1 : scale_division.round
-
end
-
-
rv = []
-
maxvalue = maxvalue%scale_division == 0 ?
-
maxvalue : maxvalue + scale_division
-
minvalue.step( maxvalue, scale_division ) {|v| rv << v}
-
return rv
-
end
-
-
1
def x_label_offset( width )
-
width / 2.0
-
end
-
-
1
def draw_data
-
minvalue = min_value
-
fieldwidth = field_width
-
-
unit_size = (@graph_height.to_f - font_size*2*top_font) /
-
(get_y_labels.max - get_y_labels.min)
-
bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0
-
-
bar_width = fieldwidth - bargap
-
bar_width /= @data.length if stack == :side
-
x_mod = (@graph_width-bargap)/2 - (stack==:side ? bar_width/2 : 0)
-
-
bottom = @graph_height
-
-
field_count = 0
-
@config[:fields].each_index { |i|
-
dataset_count = 0
-
for dataset in @data
-
-
# cases (assume 0 = +ve):
-
# value min length
-
# +ve +ve value - min
-
# +ve -ve value - 0
-
# -ve -ve value.abs - 0
-
-
value = dataset[:data][i]
-
-
left = (fieldwidth * field_count)
-
-
length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
-
# top is 0 if value is negative
-
top = bottom - (((value < 0 ? 0 : value) - minvalue) * unit_size)
-
left += bar_width * dataset_count if stack == :side
-
-
@graph.add_element( "rect", {
-
"x" => left.to_s,
-
"y" => top.to_s,
-
"width" => bar_width.to_s,
-
"height" => length.to_s,
-
"class" => "fill#{dataset_count+1}"
-
})
-
-
make_datapoint_text(left + bar_width/2.0, top - 6, value.to_s)
-
dataset_count += 1
-
end
-
field_count += 1
-
}
-
end
-
end
-
end
-
end
-
1
require 'rexml/document'
-
1
require 'SVG/Graph/Graph'
-
-
1
module SVG
-
1
module Graph
-
# = Synopsis
-
#
-
# A superclass for bar-style graphs. Do not attempt to instantiate
-
# directly; use one of the subclasses instead.
-
#
-
# = Author
-
#
-
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
-
#
-
# Copyright 2004 Sean E. Russell
-
# This software is available under the Ruby license[LICENSE.txt]
-
#
-
1
class BarBase < SVG::Graph::Graph
-
# Ensures that :fields are provided in the configuration.
-
1
def initialize config
-
raise "fields was not supplied or is empty" unless config[:fields] &&
-
config[:fields].kind_of?(Array) &&
-
config[:fields].length > 0
-
super
-
end
-
-
# In addition to the defaults set in Graph::initialize, sets
-
# [bar_gap] true
-
# [stack] :overlap
-
1
def set_defaults
-
init_with( :bar_gap => true, :stack => :overlap )
-
end
-
-
# Whether to have a gap between the bars or not, default
-
# is true, set to false if you don't want gaps.
-
1
attr_accessor :bar_gap
-
# How to stack data sets. :overlap overlaps bars with
-
# transparent colors, :top stacks bars on top of one another,
-
# :side stacks the bars side-by-side. Defaults to :overlap.
-
1
attr_accessor :stack
-
-
-
1
protected
-
-
1
def max_value
-
@data.collect{|x| x[:data].max}.max
-
end
-
-
1
def min_value
-
min = 0
-
if min_scale_value.nil?
-
min = @data.collect{|x| x[:data].min}.min
-
min = min > 0 ? 0 : min
-
else
-
min = min_scale_value
-
end
-
return min
-
end
-
-
1
def get_css
-
return <<EOL
-
/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
-
.key1,.fill1{
-
fill: #ff0000;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 0.5px;
-
}
-
.key2,.fill2{
-
fill: #0000ff;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key3,.fill3{
-
fill: #00ff00;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key4,.fill4{
-
fill: #ffcc00;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key5,.fill5{
-
fill: #00ccff;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key6,.fill6{
-
fill: #ff00ff;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key7,.fill7{
-
fill: #00ffff;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key8,.fill8{
-
fill: #ffff00;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key9,.fill9{
-
fill: #cc6666;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key10,.fill10{
-
fill: #663399;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key11,.fill11{
-
fill: #339900;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key12,.fill12{
-
fill: #9966FF;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
EOL
-
end
-
end
-
end
-
end
-
1
require 'rexml/document'
-
1
require 'SVG/Graph/BarBase'
-
-
1
module SVG
-
1
module Graph
-
# === Create presentation quality SVG horitonzal bar graphs easily
-
#
-
# = Synopsis
-
#
-
# require 'SVG/Graph/BarHorizontal'
-
#
-
# fields = %w(Jan Feb Mar)
-
# data_sales_02 = [12, 45, 21]
-
#
-
# graph = SVG::Graph::BarHorizontal.new({
-
# :height => 500,
-
# :width => 300,
-
# :fields => fields,
-
# })
-
#
-
# graph.add_data({
-
# :data => data_sales_02,
-
# :title => 'Sales 2002',
-
# })
-
#
-
# print "Content-type: image/svg+xml\r\n\r\n"
-
# print graph.burn
-
#
-
# = Description
-
#
-
# This object aims to allow you to easily create high quality
-
# SVG horitonzal bar graphs. You can either use the default style sheet
-
# or supply your own. Either way there are many options which can
-
# be configured to give you control over how the graph is
-
# generated - with or without a key, data elements at each point,
-
# title, subtitle etc.
-
#
-
# = Examples
-
#
-
# * http://germane-software.com/repositories/public/SVG/test/test.rb
-
#
-
# = See also
-
#
-
# * SVG::Graph::Graph
-
# * SVG::Graph::Bar
-
# * SVG::Graph::Line
-
# * SVG::Graph::Pie
-
# * SVG::Graph::Plot
-
# * SVG::Graph::TimeSeries
-
#
-
# == Author
-
#
-
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
-
#
-
# Copyright 2004 Sean E. Russell
-
# This software is available under the Ruby license[LICENSE.txt]
-
#
-
1
class BarHorizontal < BarBase
-
# In addition to the defaults set in BarBase::set_defaults, sets
-
# [rotate_y_labels] true
-
# [show_x_guidelines] true
-
# [show_y_guidelines] false
-
1
def set_defaults
-
super
-
init_with(
-
:rotate_y_labels => true,
-
:show_x_guidelines => true,
-
:show_y_guidelines => false
-
)
-
self.right_align = self.right_font = 1
-
end
-
-
1
protected
-
-
1
def get_x_labels
-
maxvalue = max_value
-
minvalue = min_value
-
range = maxvalue - minvalue
-
top_pad = range == 0 ? 10 : range / 20.0
-
scale_range = (maxvalue + top_pad) - minvalue
-
-
scale_division = scale_divisions || (scale_range / 10.0)
-
-
if scale_integers
-
scale_division = scale_division < 1 ? 1 : scale_division.round
-
end
-
-
rv = []
-
maxvalue = maxvalue%scale_division == 0 ?
-
maxvalue : maxvalue + scale_division
-
minvalue.step( maxvalue, scale_division ) {|v| rv << v}
-
return rv
-
end
-
-
1
def get_y_labels
-
@config[:fields]
-
end
-
-
1
def y_label_offset( height )
-
height / -2.0
-
end
-
-
1
def draw_data
-
minvalue = min_value
-
fieldheight = field_height
-
-
unit_size = (@graph_width.to_f - font_size*2*right_font ) /
-
(get_x_labels.max - get_x_labels.min )
-
bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
-
-
bar_height = fieldheight - bargap
-
bar_height /= @data.length if stack == :side
-
y_mod = (bar_height / 2) + (font_size / 2)
-
-
field_count = 1
-
@config[:fields].each_index { |i|
-
dataset_count = 0
-
for dataset in @data
-
value = dataset[:data][i]
-
-
top = @graph_height - (fieldheight * field_count)
-
top += (bar_height * dataset_count) if stack == :side
-
# cases (assume 0 = +ve):
-
# value min length left
-
# +ve +ve value.abs - min minvalue.abs
-
# +ve -ve value.abs - 0 minvalue.abs
-
# -ve -ve value.abs - 0 minvalue.abs + value
-
length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
-
left = (minvalue.abs + (value < 0 ? value : 0)) * unit_size
-
-
@graph.add_element( "rect", {
-
"x" => left.to_s,
-
"y" => top.to_s,
-
"width" => length.to_s,
-
"height" => bar_height.to_s,
-
"class" => "fill#{dataset_count+1}"
-
})
-
-
make_datapoint_text(
-
left+length+5, top+y_mod, value, "text-anchor: start; "
-
)
-
dataset_count += 1
-
end
-
field_count += 1
-
}
-
end
-
end
-
end
-
end
-
1
begin
-
1
require 'zlib'
-
1
@@__have_zlib = true
-
rescue
-
@@__have_zlib = false
-
end
-
-
1
require 'rexml/document'
-
-
1
module SVG
-
1
module Graph
-
1
VERSION = '@ANT_VERSION@'
-
-
# === Base object for generating SVG Graphs
-
#
-
# == Synopsis
-
#
-
# This class is only used as a superclass of specialized charts. Do not
-
# attempt to use this class directly, unless creating a new chart type.
-
#
-
# For examples of how to subclass this class, see the existing specific
-
# subclasses, such as SVG::Graph::Pie.
-
#
-
# == Examples
-
#
-
# For examples of how to use this package, see either the test files, or
-
# the documentation for the specific class you want to use.
-
#
-
# * file:test/plot.rb
-
# * file:test/single.rb
-
# * file:test/test.rb
-
# * file:test/timeseries.rb
-
#
-
# == Description
-
#
-
# This package should be used as a base for creating SVG graphs.
-
#
-
# == Acknowledgements
-
#
-
# Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
-
# port is based on.
-
#
-
# Stephen Morgan for creating the TT template and SVG.
-
#
-
# == See
-
#
-
# * SVG::Graph::BarHorizontal
-
# * SVG::Graph::Bar
-
# * SVG::Graph::Line
-
# * SVG::Graph::Pie
-
# * SVG::Graph::Plot
-
# * SVG::Graph::TimeSeries
-
#
-
# == Author
-
#
-
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
-
#
-
# Copyright 2004 Sean E. Russell
-
# This software is available under the Ruby license[LICENSE.txt]
-
#
-
1
class Graph
-
1
include REXML
-
-
# Initialize the graph object with the graph settings. You won't
-
# instantiate this class directly; see the subclass for options.
-
# [width] 500
-
# [height] 300
-
# [show_x_guidelines] false
-
# [show_y_guidelines] true
-
# [show_data_values] true
-
# [min_scale_value] 0
-
# [show_x_labels] true
-
# [stagger_x_labels] false
-
# [rotate_x_labels] false
-
# [step_x_labels] 1
-
# [step_include_first_x_label] true
-
# [show_y_labels] true
-
# [rotate_y_labels] false
-
# [scale_integers] false
-
# [show_x_title] false
-
# [x_title] 'X Field names'
-
# [show_y_title] false
-
# [y_title_text_direction] :bt
-
# [y_title] 'Y Scale'
-
# [show_graph_title] false
-
# [graph_title] 'Graph Title'
-
# [show_graph_subtitle] false
-
# [graph_subtitle] 'Graph Sub Title'
-
# [key] true,
-
# [key_position] :right, # bottom or righ
-
# [font_size] 12
-
# [title_font_size] 16
-
# [subtitle_font_size] 14
-
# [x_label_font_size] 12
-
# [x_title_font_size] 14
-
# [y_label_font_size] 12
-
# [y_title_font_size] 14
-
# [key_font_size] 10
-
# [no_css] false
-
# [add_popups] false
-
1
def initialize( config )
-
@config = config
-
-
self.top_align = self.top_font = self.right_align = self.right_font = 0
-
-
init_with({
-
:width => 500,
-
:height => 300,
-
:show_x_guidelines => false,
-
:show_y_guidelines => true,
-
:show_data_values => true,
-
-
# :min_scale_value => 0,
-
-
:show_x_labels => true,
-
:stagger_x_labels => false,
-
:rotate_x_labels => false,
-
:step_x_labels => 1,
-
:step_include_first_x_label => true,
-
-
:show_y_labels => true,
-
:rotate_y_labels => false,
-
:stagger_y_labels => false,
-
:scale_integers => false,
-
-
:show_x_title => false,
-
:x_title => 'X Field names',
-
-
:show_y_title => false,
-
:y_title_text_direction => :bt,
-
:y_title => 'Y Scale',
-
-
:show_graph_title => false,
-
:graph_title => 'Graph Title',
-
:show_graph_subtitle => false,
-
:graph_subtitle => 'Graph Sub Title',
-
:key => true,
-
:key_position => :right, # bottom or right
-
-
:font_size =>12,
-
:title_font_size =>16,
-
:subtitle_font_size =>14,
-
:x_label_font_size =>12,
-
:x_title_font_size =>14,
-
:y_label_font_size =>12,
-
:y_title_font_size =>14,
-
:key_font_size =>10,
-
-
:no_css =>false,
-
:add_popups =>false,
-
})
-
-
set_defaults if respond_to? :set_defaults
-
-
init_with config
-
end
-
-
-
# This method allows you do add data to the graph object.
-
# It can be called several times to add more data sets in.
-
#
-
# data_sales_02 = [12, 45, 21];
-
#
-
# graph.add_data({
-
# :data => data_sales_02,
-
# :title => 'Sales 2002'
-
# })
-
1
def add_data conf
-
@data = [] unless defined? @data
-
-
if conf[:data] and conf[:data].kind_of? Array
-
@data << conf
-
else
-
raise "No data provided by #{conf.inspect}"
-
end
-
end
-
-
-
# This method removes all data from the object so that you can
-
# reuse it to create a new graph but with the same config options.
-
#
-
# graph.clear_data
-
1
def clear_data
-
@data = []
-
end
-
-
-
# This method processes the template with the data and
-
# config which has been set and returns the resulting SVG.
-
#
-
# This method will croak unless at least one data set has
-
# been added to the graph object.
-
#
-
# print graph.burn
-
1
def burn
-
raise "No data available" unless @data.size > 0
-
-
calculations if respond_to? :calculations
-
-
start_svg
-
calculate_graph_dimensions
-
@foreground = Element.new( "g" )
-
draw_graph
-
draw_titles
-
draw_legend
-
draw_data
-
@graph.add_element( @foreground )
-
style
-
-
data = ""
-
@doc.write( data, 0 )
-
-
if @config[:compress]
-
if @@__have_zlib
-
inp, out = IO.pipe
-
gz = Zlib::GzipWriter.new( out )
-
gz.write data
-
gz.close
-
data = inp.read
-
else
-
data << "<!-- Ruby Zlib not available for SVGZ -->";
-
end
-
end
-
-
return data
-
end
-
-
-
# Set the height of the graph box, this is the total height
-
# of the SVG box created - not the graph it self which auto
-
# scales to fix the space.
-
1
attr_accessor :height
-
# Set the width of the graph box, this is the total width
-
# of the SVG box created - not the graph it self which auto
-
# scales to fix the space.
-
1
attr_accessor :width
-
# Set the path to an external stylesheet, set to '' if
-
# you want to revert back to using the defaut internal version.
-
#
-
# To create an external stylesheet create a graph using the
-
# default internal version and copy the stylesheet section to
-
# an external file and edit from there.
-
1
attr_accessor :style_sheet
-
# (Bool) Show the value of each element of data on the graph
-
1
attr_accessor :show_data_values
-
# The point at which the Y axis starts, defaults to '0',
-
# if set to nil it will default to the minimum data value.
-
1
attr_accessor :min_scale_value
-
# Whether to show labels on the X axis or not, defaults
-
# to true, set to false if you want to turn them off.
-
1
attr_accessor :show_x_labels
-
# This puts the X labels at alternative levels so if they
-
# are long field names they will not overlap so easily.
-
# Default it false, to turn on set to true.
-
1
attr_accessor :stagger_x_labels
-
# This puts the Y labels at alternative levels so if they
-
# are long field names they will not overlap so easily.
-
# Default it false, to turn on set to true.
-
1
attr_accessor :stagger_y_labels
-
# This turns the X axis labels by 90 degrees.
-
# Default it false, to turn on set to true.
-
1
attr_accessor :rotate_x_labels
-
# This turns the Y axis labels by 90 degrees.
-
# Default it false, to turn on set to true.
-
1
attr_accessor :rotate_y_labels
-
# How many "steps" to use between displayed X axis labels,
-
# a step of one means display every label, a step of two results
-
# in every other label being displayed (label <gap> label <gap> label),
-
# a step of three results in every third label being displayed
-
# (label <gap> <gap> label <gap> <gap> label) and so on.
-
1
attr_accessor :step_x_labels
-
# Whether to (when taking "steps" between X axis labels) step from
-
# the first label (i.e. always include the first label) or step from
-
# the X axis origin (i.e. start with a gap if step_x_labels is greater
-
# than one).
-
1
attr_accessor :step_include_first_x_label
-
# Whether to show labels on the Y axis or not, defaults
-
# to true, set to false if you want to turn them off.
-
1
attr_accessor :show_y_labels
-
# Ensures only whole numbers are used as the scale divisions.
-
# Default it false, to turn on set to true. This has no effect if
-
# scale divisions are less than 1.
-
1
attr_accessor :scale_integers
-
# This defines the gap between markers on the Y axis,
-
# default is a 10th of the max_value, e.g. you will have
-
# 10 markers on the Y axis. NOTE: do not set this too
-
# low - you are limited to 999 markers, after that the
-
# graph won't generate.
-
1
attr_accessor :scale_divisions
-
# Whether to show the title under the X axis labels,
-
# default is false, set to true to show.
-
1
attr_accessor :show_x_title
-
# What the title under X axis should be, e.g. 'Months'.
-
1
attr_accessor :x_title
-
# Whether to show the title under the Y axis labels,
-
# default is false, set to true to show.
-
1
attr_accessor :show_y_title
-
# Aligns writing mode for Y axis label.
-
# Defaults to :bt (Bottom to Top).
-
# Change to :tb (Top to Bottom) to reverse.
-
1
attr_accessor :y_title_text_direction
-
# What the title under Y axis should be, e.g. 'Sales in thousands'.
-
1
attr_accessor :y_title
-
# Whether to show a title on the graph, defaults
-
# to false, set to true to show.
-
1
attr_accessor :show_graph_title
-
# What the title on the graph should be.
-
1
attr_accessor :graph_title
-
# Whether to show a subtitle on the graph, defaults
-
# to false, set to true to show.
-
1
attr_accessor :show_graph_subtitle
-
# What the subtitle on the graph should be.
-
1
attr_accessor :graph_subtitle
-
# Whether to show a key, defaults to false, set to
-
# true if you want to show it.
-
1
attr_accessor :key
-
# Where the key should be positioned, defaults to
-
# :right, set to :bottom if you want to move it.
-
1
attr_accessor :key_position
-
# Set the font size (in points) of the data point labels
-
1
attr_accessor :font_size
-
# Set the font size of the X axis labels
-
1
attr_accessor :x_label_font_size
-
# Set the font size of the X axis title
-
1
attr_accessor :x_title_font_size
-
# Set the font size of the Y axis labels
-
1
attr_accessor :y_label_font_size
-
# Set the font size of the Y axis title
-
1
attr_accessor :y_title_font_size
-
# Set the title font size
-
1
attr_accessor :title_font_size
-
# Set the subtitle font size
-
1
attr_accessor :subtitle_font_size
-
# Set the key font size
-
1
attr_accessor :key_font_size
-
# Show guidelines for the X axis
-
1
attr_accessor :show_x_guidelines
-
# Show guidelines for the Y axis
-
1
attr_accessor :show_y_guidelines
-
# Do not use CSS if set to true. Many SVG viewers do not support CSS, but
-
# not using CSS can result in larger SVGs as well as making it impossible to
-
# change colors after the chart is generated. Defaults to false.
-
1
attr_accessor :no_css
-
# Add popups for the data points on some graphs
-
1
attr_accessor :add_popups
-
-
-
1
protected
-
-
1
def sort( *arrys )
-
sort_multiple( arrys )
-
end
-
-
# Overwrite configuration options with supplied options. Used
-
# by subclasses.
-
1
def init_with config
-
config.each { |key, value|
-
self.send((key.to_s+"=").to_sym, value ) if respond_to? key.to_sym
-
}
-
end
-
-
1
attr_accessor :top_align, :top_font, :right_align, :right_font
-
-
1
KEY_BOX_SIZE = 12
-
-
# Override this (and call super) to change the margin to the left
-
# of the plot area. Results in @border_left being set.
-
1
def calculate_left_margin
-
@border_left = 7
-
# Check for Y labels
-
max_y_label_height_px = rotate_y_labels ?
-
y_label_font_size :
-
get_y_labels.max{|a,b|
-
a.to_s.length<=>b.to_s.length
-
}.to_s.length * y_label_font_size * 0.6
-
@border_left += max_y_label_height_px if show_y_labels
-
@border_left += max_y_label_height_px + 10 if stagger_y_labels
-
@border_left += y_title_font_size + 5 if show_y_title
-
end
-
-
-
# Calculates the width of the widest Y label. This will be the
-
# character height if the Y labels are rotated
-
1
def max_y_label_width_px
-
return font_size if rotate_y_labels
-
end
-
-
-
# Override this (and call super) to change the margin to the right
-
# of the plot area. Results in @border_right being set.
-
1
def calculate_right_margin
-
@border_right = 7
-
if key and key_position == :right
-
val = keys.max { |a,b| a.length <=> b.length }
-
@border_right += val.length * key_font_size * 0.6
-
@border_right += KEY_BOX_SIZE
-
@border_right += 10 # Some padding around the box
-
end
-
end
-
-
-
# Override this (and call super) to change the margin to the top
-
# of the plot area. Results in @border_top being set.
-
1
def calculate_top_margin
-
@border_top = 5
-
@border_top += title_font_size if show_graph_title
-
@border_top += 5
-
@border_top += subtitle_font_size if show_graph_subtitle
-
end
-
-
-
# Adds pop-up point information to a graph.
-
1
def add_popup( x, y, label )
-
txt_width = label.length * font_size * 0.6 + 10
-
tx = (x+txt_width > width ? x-5 : x+5)
-
t = @foreground.add_element( "text", {
-
"x" => tx.to_s,
-
"y" => (y - font_size).to_s,
-
"visibility" => "hidden",
-
})
-
t.attributes["style"] = "fill: #000; "+
-
(x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
-
t.text = label.to_s
-
t.attributes["id"] = t.object_id.to_s
-
-
@foreground.add_element( "circle", {
-
"cx" => x.to_s,
-
"cy" => y.to_s,
-
"r" => "10",
-
"style" => "opacity: 0",
-
"onmouseover" =>
-
"document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )",
-
"onmouseout" =>
-
"document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",
-
})
-
-
end
-
-
-
# Override this (and call super) to change the margin to the bottom
-
# of the plot area. Results in @border_bottom being set.
-
1
def calculate_bottom_margin
-
@border_bottom = 7
-
if key and key_position == :bottom
-
@border_bottom += @data.size * (font_size + 5)
-
@border_bottom += 10
-
end
-
if show_x_labels
-
max_x_label_height_px = (not rotate_x_labels) ?
-
x_label_font_size :
-
get_x_labels.max{|a,b|
-
a.to_s.length<=>b.to_s.length
-
}.to_s.length * x_label_font_size * 0.6
-
@border_bottom += max_x_label_height_px
-
@border_bottom += max_x_label_height_px + 10 if stagger_x_labels
-
end
-
@border_bottom += x_title_font_size + 5 if show_x_title
-
end
-
-
-
# Draws the background, axis, and labels.
-
1
def draw_graph
-
@graph = @root.add_element( "g", {
-
"transform" => "translate( #@border_left #@border_top )"
-
})
-
-
# Background
-
@graph.add_element( "rect", {
-
"x" => "0",
-
"y" => "0",
-
"width" => @graph_width.to_s,
-
"height" => @graph_height.to_s,
-
"class" => "graphBackground"
-
})
-
-
# Axis
-
@graph.add_element( "path", {
-
"d" => "M 0 0 v#@graph_height",
-
"class" => "axis",
-
"id" => "xAxis"
-
})
-
@graph.add_element( "path", {
-
"d" => "M 0 #@graph_height h#@graph_width",
-
"class" => "axis",
-
"id" => "yAxis"
-
})
-
-
draw_x_labels
-
draw_y_labels
-
end
-
-
-
# Where in the X area the label is drawn
-
# Centered in the field, should be width/2. Start, 0.
-
1
def x_label_offset( width )
-
0
-
end
-
-
1
def make_datapoint_text( x, y, value, style="" )
-
if show_data_values
-
@foreground.add_element( "text", {
-
"x" => x.to_s,
-
"y" => y.to_s,
-
"class" => "dataPointLabel",
-
"style" => "#{style} stroke: #fff; stroke-width: 2;"
-
}).text = value.to_s
-
text = @foreground.add_element( "text", {
-
"x" => x.to_s,
-
"y" => y.to_s,
-
"class" => "dataPointLabel"
-
})
-
text.text = value.to_s
-
text.attributes["style"] = style if style.length > 0
-
end
-
end
-
-
-
# Draws the X axis labels
-
1
def draw_x_labels
-
stagger = x_label_font_size + 5
-
if show_x_labels
-
label_width = field_width
-
-
count = 0
-
for label in get_x_labels
-
if step_include_first_x_label == true then
-
step = count % step_x_labels
-
else
-
step = (count + 1) % step_x_labels
-
end
-
-
if step == 0 then
-
text = @graph.add_element( "text" )
-
text.attributes["class"] = "xAxisLabels"
-
text.text = label.to_s
-
-
x = count * label_width + x_label_offset( label_width )
-
y = @graph_height + x_label_font_size + 3
-
t = 0 - (font_size / 2)
-
-
if stagger_x_labels and count % 2 == 1
-
y += stagger
-
@graph.add_element( "path", {
-
"d" => "M#{x} #@graph_height v#{stagger}",
-
"class" => "staggerGuideLine"
-
})
-
end
-
-
text.attributes["x"] = x.to_s
-
text.attributes["y"] = y.to_s
-
if rotate_x_labels
-
text.attributes["transform"] =
-
"rotate( 90 #{x} #{y-x_label_font_size} )"+
-
" translate( 0 -#{x_label_font_size/4} )"
-
text.attributes["style"] = "text-anchor: start"
-
else
-
text.attributes["style"] = "text-anchor: middle"
-
end
-
end
-
-
draw_x_guidelines( label_width, count ) if show_x_guidelines
-
count += 1
-
end
-
end
-
end
-
-
-
# Where in the Y area the label is drawn
-
# Centered in the field, should be width/2. Start, 0.
-
1
def y_label_offset( height )
-
0
-
end
-
-
-
1
def field_width
-
(@graph_width.to_f - font_size*2*right_font) /
-
(get_x_labels.length - right_align)
-
end
-
-
-
1
def field_height
-
(@graph_height.to_f - font_size*2*top_font) /
-
(get_y_labels.length - top_align)
-
end
-
-
-
# Draws the Y axis labels
-
1
def draw_y_labels
-
stagger = y_label_font_size + 5
-
if show_y_labels
-
label_height = field_height
-
-
count = 0
-
y_offset = @graph_height + y_label_offset( label_height )
-
y_offset += font_size/1.2 unless rotate_y_labels
-
for label in get_y_labels
-
y = y_offset - (label_height * count)
-
x = rotate_y_labels ? 0 : -3
-
-
if stagger_y_labels and count % 2 == 1
-
x -= stagger
-
@graph.add_element( "path", {
-
"d" => "M#{x} #{y} h#{stagger}",
-
"class" => "staggerGuideLine"
-
})
-
end
-
-
text = @graph.add_element( "text", {
-
"x" => x.to_s,
-
"y" => y.to_s,
-
"class" => "yAxisLabels"
-
})
-
text.text = label.to_s
-
if rotate_y_labels
-
text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
-
"rotate( 90 #{x} #{y} ) "
-
text.attributes["style"] = "text-anchor: middle"
-
else
-
text.attributes["y"] = (y - (y_label_font_size/2)).to_s
-
text.attributes["style"] = "text-anchor: end"
-
end
-
draw_y_guidelines( label_height, count ) if show_y_guidelines
-
count += 1
-
end
-
end
-
end
-
-
-
# Draws the X axis guidelines
-
1
def draw_x_guidelines( label_height, count )
-
if count != 0
-
@graph.add_element( "path", {
-
"d" => "M#{label_height*count} 0 v#@graph_height",
-
"class" => "guideLines"
-
})
-
end
-
end
-
-
-
# Draws the Y axis guidelines
-
1
def draw_y_guidelines( label_height, count )
-
if count != 0
-
@graph.add_element( "path", {
-
"d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
-
"class" => "guideLines"
-
})
-
end
-
end
-
-
-
# Draws the graph title and subtitle
-
1
def draw_titles
-
if show_graph_title
-
@root.add_element( "text", {
-
"x" => (width / 2).to_s,
-
"y" => (title_font_size).to_s,
-
"class" => "mainTitle"
-
}).text = graph_title.to_s
-
end
-
-
if show_graph_subtitle
-
y_subtitle = show_graph_title ?
-
title_font_size + 10 :
-
subtitle_font_size
-
@root.add_element("text", {
-
"x" => (width / 2).to_s,
-
"y" => (y_subtitle).to_s,
-
"class" => "subTitle"
-
}).text = graph_subtitle.to_s
-
end
-
-
if show_x_title
-
y = @graph_height + @border_top + x_title_font_size
-
if show_x_labels
-
y += x_label_font_size + 5 if stagger_x_labels
-
y += x_label_font_size + 5
-
end
-
x = width / 2
-
-
@root.add_element("text", {
-
"x" => x.to_s,
-
"y" => y.to_s,
-
"class" => "xAxisTitle",
-
}).text = x_title.to_s
-
end
-
-
if show_y_title
-
x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
-
y = height / 2
-
-
text = @root.add_element("text", {
-
"x" => x.to_s,
-
"y" => y.to_s,
-
"class" => "yAxisTitle",
-
})
-
text.text = y_title.to_s
-
if y_title_text_direction == :bt
-
text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
-
else
-
text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
-
end
-
end
-
end
-
-
1
def keys
-
return @data.collect{ |d| d[:title] }
-
end
-
-
# Draws the legend on the graph
-
1
def draw_legend
-
if key
-
group = @root.add_element( "g" )
-
-
key_count = 0
-
for key_name in keys
-
y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
-
group.add_element( "rect", {
-
"x" => 0.to_s,
-
"y" => y_offset.to_s,
-
"width" => KEY_BOX_SIZE.to_s,
-
"height" => KEY_BOX_SIZE.to_s,
-
"class" => "key#{key_count+1}"
-
})
-
group.add_element( "text", {
-
"x" => (KEY_BOX_SIZE + 5).to_s,
-
"y" => (y_offset + KEY_BOX_SIZE).to_s,
-
"class" => "keyText"
-
}).text = key_name.to_s
-
key_count += 1
-
end
-
-
case key_position
-
when :right
-
x_offset = @graph_width + @border_left + 10
-
y_offset = @border_top + 20
-
when :bottom
-
x_offset = @border_left + 20
-
y_offset = @border_top + @graph_height + 5
-
if show_x_labels
-
max_x_label_height_px = (not rotate_x_labels) ?
-
x_label_font_size :
-
get_x_labels.max{|a,b|
-
a.to_s.length<=>b.to_s.length
-
}.to_s.length * x_label_font_size * 0.6
-
x_label_font_size
-
y_offset += max_x_label_height_px
-
y_offset += max_x_label_height_px + 5 if stagger_x_labels
-
end
-
y_offset += x_title_font_size + 5 if show_x_title
-
end
-
group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
-
end
-
end
-
-
-
1
private
-
-
1
def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
-
if lo < hi
-
p = partition(arrys,lo,hi)
-
sort_multiple(arrys, lo, p-1)
-
sort_multiple(arrys, p+1, hi)
-
end
-
arrys
-
end
-
-
1
def partition( arrys, lo, hi )
-
p = arrys[0][lo]
-
l = lo
-
z = lo+1
-
while z <= hi
-
if arrys[0][z] < p
-
l += 1
-
arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
-
end
-
z += 1
-
end
-
arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
-
l
-
end
-
-
1
def style
-
if no_css
-
styles = parse_css
-
@root.elements.each("//*[@class]") { |el|
-
cl = el.attributes["class"]
-
style = styles[cl]
-
style += el.attributes["style"] if el.attributes["style"]
-
el.attributes["style"] = style
-
}
-
end
-
end
-
-
1
def parse_css
-
css = get_style
-
rv = {}
-
while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
-
names_orig = names = $1
-
css = $'
-
css =~ /([^}]+)\}/m
-
content = $1
-
css = $'
-
-
nms = []
-
while names =~ /^\s*,?\s*\.(\w+)/
-
nms << $1
-
names = $'
-
end
-
-
content = content.tr( "\n\t", " ")
-
for name in nms
-
current = rv[name]
-
current = current ? current+"; "+content : content
-
rv[name] = current.strip.squeeze(" ")
-
end
-
end
-
return rv
-
end
-
-
-
# Override and place code to add defs here
-
1
def add_defs defs
-
end
-
-
-
1
def start_svg
-
# Base document
-
@doc = Document.new
-
@doc << XMLDecl.new
-
@doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
-
%q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
-
if style_sheet && style_sheet != ''
-
@doc << Instruction.new( "xml-stylesheet",
-
%Q{href="#{style_sheet}" type="text/css"} )
-
end
-
@root = @doc.add_element( "svg", {
-
"width" => width.to_s,
-
"height" => height.to_s,
-
"viewBox" => "0 0 #{width} #{height}",
-
"xmlns" => "http://www.w3.org/2000/svg",
-
"xmlns:xlink" => "http://www.w3.org/1999/xlink",
-
"xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
-
"a3:scriptImplementation" => "Adobe"
-
})
-
@root << Comment.new( " "+"\\"*66 )
-
@root << Comment.new( " Created with SVG::Graph " )
-
@root << Comment.new( " SVG::Graph by Sean E. Russell " )
-
@root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
-
" Leo Lapworth & Stephan Morgan " )
-
@root << Comment.new( " "+"/"*66 )
-
-
defs = @root.add_element( "defs" )
-
add_defs defs
-
if not(style_sheet && style_sheet != '') and !no_css
-
@root << Comment.new(" include default stylesheet if none specified ")
-
style = defs.add_element( "style", {"type"=>"text/css"} )
-
style << CData.new( get_style )
-
end
-
-
@root << Comment.new( "SVG Background" )
-
@root.add_element( "rect", {
-
"width" => width.to_s,
-
"height" => height.to_s,
-
"x" => "0",
-
"y" => "0",
-
"class" => "svgBackground"
-
})
-
end
-
-
-
1
def calculate_graph_dimensions
-
calculate_left_margin
-
calculate_right_margin
-
calculate_bottom_margin
-
calculate_top_margin
-
@graph_width = width - @border_left - @border_right
-
@graph_height = height - @border_top - @border_bottom
-
end
-
-
1
def get_style
-
return <<EOL
-
/* Copy from here for external style sheet */
-
.svgBackground{
-
fill:#ffffff;
-
}
-
.graphBackground{
-
fill:#f0f0f0;
-
}
-
-
/* graphs titles */
-
.mainTitle{
-
text-anchor: middle;
-
fill: #000000;
-
font-size: #{title_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
.subTitle{
-
text-anchor: middle;
-
fill: #999999;
-
font-size: #{subtitle_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.axis{
-
stroke: #000000;
-
stroke-width: 1px;
-
}
-
-
.guideLines{
-
stroke: #666666;
-
stroke-width: 1px;
-
stroke-dasharray: 5 5;
-
}
-
-
.xAxisLabels{
-
text-anchor: middle;
-
fill: #000000;
-
font-size: #{x_label_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.yAxisLabels{
-
text-anchor: end;
-
fill: #000000;
-
font-size: #{y_label_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.xAxisTitle{
-
text-anchor: middle;
-
fill: #ff0000;
-
font-size: #{x_title_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.yAxisTitle{
-
fill: #ff0000;
-
text-anchor: middle;
-
font-size: #{y_title_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.dataPointLabel{
-
fill: #000000;
-
text-anchor:middle;
-
font-size: 10px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.staggerGuideLine{
-
fill: none;
-
stroke: #000000;
-
stroke-width: 0.5px;
-
}
-
-
#{get_css}
-
-
.keyText{
-
fill: #000000;
-
text-anchor:start;
-
font-size: #{key_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
/* End copy for external style sheet */
-
EOL
-
end
-
-
end
-
end
-
end
-
1
module RedmineDiff
-
1
class Diff
-
-
1
VERSION = 0.3
-
-
1
def Diff.lcs(a, b)
-
astart = 0
-
bstart = 0
-
afinish = a.length-1
-
bfinish = b.length-1
-
mvector = []
-
-
# First we prune off any common elements at the beginning
-
while (astart <= afinish && bstart <= afinish && a[astart] == b[bstart])
-
mvector[astart] = bstart
-
astart += 1
-
bstart += 1
-
end
-
-
# now the end
-
while (astart <= afinish && bstart <= bfinish && a[afinish] == b[bfinish])
-
mvector[afinish] = bfinish
-
afinish -= 1
-
bfinish -= 1
-
end
-
-
bmatches = b.reverse_hash(bstart..bfinish)
-
thresh = []
-
links = []
-
-
(astart..afinish).each { |aindex|
-
aelem = a[aindex]
-
next unless bmatches.has_key? aelem
-
k = nil
-
bmatches[aelem].reverse.each { |bindex|
-
if k && (thresh[k] > bindex) && (thresh[k-1] < bindex)
-
thresh[k] = bindex
-
else
-
k = thresh.replacenextlarger(bindex, k)
-
end
-
links[k] = [ (k==0) ? nil : links[k-1], aindex, bindex ] if k
-
}
-
}
-
-
if !thresh.empty?
-
link = links[thresh.length-1]
-
while link
-
mvector[link[1]] = link[2]
-
link = link[0]
-
end
-
end
-
-
return mvector
-
end
-
-
1
def makediff(a, b)
-
mvector = Diff.lcs(a, b)
-
ai = bi = 0
-
while ai < mvector.length
-
bline = mvector[ai]
-
if bline
-
while bi < bline
-
discardb(bi, b[bi])
-
bi += 1
-
end
-
match(ai, bi)
-
bi += 1
-
else
-
discarda(ai, a[ai])
-
end
-
ai += 1
-
end
-
while ai < a.length
-
discarda(ai, a[ai])
-
ai += 1
-
end
-
while bi < b.length
-
discardb(bi, b[bi])
-
bi += 1
-
end
-
match(ai, bi)
-
1
-
end
-
-
1
def compactdiffs
-
diffs = []
-
@diffs.each { |df|
-
i = 0
-
curdiff = []
-
while i < df.length
-
whot = df[i][0]
-
s = @isstring ? df[i][2].chr : [df[i][2]]
-
p = df[i][1]
-
last = df[i][1]
-
i += 1
-
while df[i] && df[i][0] == whot && df[i][1] == last+1
-
s << df[i][2]
-
last = df[i][1]
-
i += 1
-
end
-
curdiff.push [whot, p, s]
-
end
-
diffs.push curdiff
-
}
-
return diffs
-
end
-
-
1
attr_reader :diffs, :difftype
-
-
1
def initialize(diffs_or_a, b = nil, isstring = nil)
-
if b.nil?
-
@diffs = diffs_or_a
-
@isstring = isstring
-
else
-
@diffs = []
-
@curdiffs = []
-
makediff(diffs_or_a, b)
-
@difftype = diffs_or_a.class
-
end
-
end
-
-
1
def match(ai, bi)
-
@diffs.push @curdiffs unless @curdiffs.empty?
-
@curdiffs = []
-
end
-
-
1
def discarda(i, elem)
-
@curdiffs.push ['-', i, elem]
-
end
-
-
1
def discardb(i, elem)
-
@curdiffs.push ['+', i, elem]
-
end
-
-
1
def compact
-
return Diff.new(compactdiffs)
-
end
-
-
1
def compact!
-
@diffs = compactdiffs
-
end
-
-
1
def inspect
-
@diffs.inspect
-
end
-
-
end
-
end
-
-
1
module Diffable
-
1
def diff(b)
-
RedmineDiff::Diff.new(self, b)
-
end
-
-
# Create a hash that maps elements of the array to arrays of indices
-
# where the elements are found.
-
-
1
def reverse_hash(range = (0...self.length))
-
revmap = {}
-
range.each { |i|
-
elem = self[i]
-
if revmap.has_key? elem
-
revmap[elem].push i
-
else
-
revmap[elem] = [i]
-
end
-
}
-
return revmap
-
end
-
-
1
def replacenextlarger(value, high = nil)
-
high ||= self.length
-
if self.empty? || value > self[-1]
-
push value
-
return high
-
end
-
# binary search for replacement point
-
low = 0
-
while low < high
-
index = (high+low)/2
-
found = self[index]
-
return nil if value == found
-
if value > found
-
low = index + 1
-
else
-
high = index
-
end
-
end
-
-
self[low] = value
-
# $stderr << "replace #{value} : 0/#{low}/#{init_high} (#{steps} steps) (#{init_high-low} off )\n"
-
# $stderr.puts self.inspect
-
#gets
-
#p length - low
-
return low
-
end
-
-
1
def patch(diff)
-
newary = nil
-
if diff.difftype == String
-
newary = diff.difftype.new('')
-
else
-
newary = diff.difftype.new
-
end
-
ai = 0
-
bi = 0
-
diff.diffs.each { |d|
-
d.each { |mod|
-
case mod[0]
-
when '-'
-
while ai < mod[1]
-
newary << self[ai]
-
ai += 1
-
bi += 1
-
end
-
ai += 1
-
when '+'
-
while bi < mod[1]
-
newary << self[ai]
-
ai += 1
-
bi += 1
-
end
-
newary << mod[2]
-
bi += 1
-
else
-
raise "Unknown diff action"
-
end
-
}
-
}
-
while ai < self.length
-
newary << self[ai]
-
ai += 1
-
bi += 1
-
end
-
return newary
-
end
-
end
-
-
1
class Array
-
1
include Diffable
-
end
-
-
1
class String
-
1
include Diffable
-
end
-
-
=begin
-
= Diff
-
(({diff.rb})) - computes the differences between two arrays or
-
strings. Copyright (C) 2001 Lars Christensen
-
-
== Synopsis
-
-
diff = Diff.new(a, b)
-
b = a.patch(diff)
-
-
== Class Diff
-
=== Class Methods
-
--- Diff.new(a, b)
-
--- a.diff(b)
-
Creates a Diff object which represent the differences between
-
((|a|)) and ((|b|)). ((|a|)) and ((|b|)) can be either be arrays
-
of any objects, strings, or object of any class that include
-
module ((|Diffable|))
-
-
== Module Diffable
-
The module ((|Diffable|)) is intended to be included in any class for
-
which differences are to be computed. Diffable is included into String
-
and Array when (({diff.rb})) is (({require}))'d.
-
-
Classes including Diffable should implement (({[]})) to get element at
-
integer indices, (({<<})) to append elements to the object and
-
(({ClassName#new})) should accept 0 arguments to create a new empty
-
object.
-
-
=== Instance Methods
-
--- Diffable#patch(diff)
-
Applies the differences from ((|diff|)) to the object ((|obj|))
-
and return the result. ((|obj|)) is not changed. ((|obj|)) and
-
can be either an array or a string, but must match the object
-
from which the ((|diff|)) was created.
-
=end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Acts
-
1
module ActivityProvider
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def acts_as_activity_provider(options = {})
-
10
unless self.included_modules.include?(Redmine::Acts::ActivityProvider::InstanceMethods)
-
9
cattr_accessor :activity_provider_options
-
9
send :include, Redmine::Acts::ActivityProvider::InstanceMethods
-
end
-
-
10
options.assert_valid_keys(:type, :permission, :timestamp, :author_key, :find_options)
-
10
self.activity_provider_options ||= {}
-
-
# One model can provide different event types
-
# We store these options in activity_provider_options hash
-
10
event_type = options.delete(:type) || self.name.underscore.pluralize
-
-
10
options[:timestamp] ||= "#{table_name}.created_on"
-
10
options[:find_options] ||= {}
-
10
options[:author_key] = "#{table_name}.#{options[:author_key]}" if options[:author_key].is_a?(Symbol)
-
10
self.activity_provider_options[event_type] = options
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
9
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
# Returns events of type event_type visible by user that occured between from and to
-
1
def find_events(event_type, user, from, to, options)
-
provider_options = activity_provider_options[event_type]
-
raise "#{self.name} can not provide #{event_type} events." if provider_options.nil?
-
-
scope = self
-
-
if from && to
-
scope = scope.scoped(:conditions => ["#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to])
-
end
-
-
if options[:author]
-
return [] if provider_options[:author_key].nil?
-
scope = scope.scoped(:conditions => ["#{provider_options[:author_key]} = ?", options[:author].id])
-
end
-
-
if options[:limit]
-
# id and creation time should be in same order in most cases
-
scope = scope.scoped(:order => "#{table_name}.id DESC", :limit => options[:limit])
-
end
-
-
if provider_options.has_key?(:permission)
-
scope = scope.scoped(:conditions => Project.allowed_to_condition(user, provider_options[:permission] || :view_project, options))
-
elsif respond_to?(:visible)
-
scope = scope.visible(user, options)
-
else
-
ActiveSupport::Deprecation.warn "acts_as_activity_provider with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option."
-
scope = scope.scoped(:conditions => Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym, options))
-
end
-
-
scope.all(provider_options[:find_options].dup)
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Acts
-
1
module Attachable
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def acts_as_attachable(options = {})
-
7
cattr_accessor :attachable_options
-
7
self.attachable_options = {}
-
7
attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
-
7
attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
-
-
7
has_many :attachments, options.merge(:as => :container,
-
:order => "#{Attachment.table_name}.created_on ASC, #{Attachment.table_name}.id ASC",
-
:dependent => :destroy)
-
7
send :include, Redmine::Acts::Attachable::InstanceMethods
-
7
before_save :attach_saved_attachments
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
7
base.extend ClassMethods
-
end
-
-
1
def attachments_visible?(user=User.current)
-
(respond_to?(:visible?) ? visible?(user) : true) &&
-
user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
-
end
-
-
1
def attachments_deletable?(user=User.current)
-
(respond_to?(:visible?) ? visible?(user) : true) &&
-
user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
-
end
-
-
1
def saved_attachments
-
2221
@saved_attachments ||= []
-
end
-
-
1
def unsaved_attachments
-
4
@unsaved_attachments ||= []
-
end
-
-
1
def save_attachments(attachments, author=User.current)
-
2
if attachments.is_a?(Hash)
-
2
attachments = attachments.stringify_keys
-
2
attachments = attachments.to_a.sort {|a, b|
-
if a.first.to_i > 0 && b.first.to_i > 0
-
a.first.to_i <=> b.first.to_i
-
elsif a.first.to_i > 0
-
1
-
elsif b.first.to_i > 0
-
-1
-
else
-
a.first <=> b.first
-
end
-
}
-
2
attachments = attachments.map(&:last)
-
end
-
2
if attachments.is_a?(Array)
-
2
attachments.each do |attachment|
-
2
a = nil
-
2
if file = attachment['file']
-
next unless file.size > 0
-
a = Attachment.create(:file => file, :author => author)
-
elsif token = attachment['token']
-
a = Attachment.find_by_token(token)
-
next unless a
-
a.filename = attachment['filename'] unless attachment['filename'].blank?
-
a.content_type = attachment['content_type']
-
end
-
2
next unless a
-
a.description = attachment['description'].to_s.strip
-
if a.new_record?
-
unsaved_attachments << a
-
else
-
saved_attachments << a
-
end
-
end
-
end
-
2
{:files => saved_attachments, :unsaved => unsaved_attachments}
-
end
-
-
1
def attach_saved_attachments
-
2209
saved_attachments.each do |attachment|
-
self.attachments << attachment
-
end
-
end
-
-
1
module ClassMethods
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Acts
-
1
module Customizable
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def acts_as_customizable(options = {})
-
7
return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
-
7
cattr_accessor :customizable_options
-
7
self.customizable_options = options
-
7
has_many :custom_values, :as => :customized,
-
:include => :custom_field,
-
:order => "#{CustomField.table_name}.position",
-
:dependent => :delete_all,
-
:validate => false
-
7
send :include, Redmine::Acts::Customizable::InstanceMethods
-
7
validate :validate_custom_field_values
-
7
after_save :save_custom_field_values
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
7
base.extend ClassMethods
-
end
-
-
1
def available_custom_fields
-
163
CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
-
:order => 'position')
-
end
-
-
# Sets the values of the object's custom fields
-
# values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
-
1
def custom_fields=(values)
-
values_to_hash = values.inject({}) do |hash, v|
-
v = v.stringify_keys
-
if v['id'] && v.has_key?('value')
-
hash[v['id']] = v['value']
-
end
-
hash
-
end
-
self.custom_field_values = values_to_hash
-
end
-
-
# Sets the values of the object's custom fields
-
# values is a hash like {'1' => 'foo', 2 => 'bar'}
-
1
def custom_field_values=(values)
-
10
values = values.stringify_keys
-
-
10
custom_field_values.each do |custom_field_value|
-
2
key = custom_field_value.custom_field_id.to_s
-
2
if values.has_key?(key)
-
2
value = values[key]
-
2
if value.is_a?(Array)
-
value = value.reject(&:blank?).uniq
-
if value.empty?
-
value << ''
-
end
-
end
-
2
custom_field_value.value = value
-
end
-
end
-
10
@custom_field_values_changed = true
-
end
-
-
1
def custom_field_values
-
@custom_field_values ||= available_custom_fields.collect do |field|
-
566
x = CustomFieldValue.new
-
566
x.custom_field = field
-
566
x.customized = self
-
566
if field.multiple?
-
values = custom_values.select { |v| v.custom_field == field }
-
if values.empty?
-
values << custom_values.build(:customized => self, :custom_field => field, :value => nil)
-
end
-
x.value = values.map(&:value)
-
else
-
1043
cv = custom_values.detect { |v| v.custom_field == field }
-
566
cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
-
566
x.value = cv.value
-
end
-
566
x
-
6449
end
-
end
-
-
1
def visible_custom_field_values
-
62
custom_field_values.select(&:visible?)
-
end
-
-
1
def custom_field_values_changed?
-
335
@custom_field_values_changed == true
-
end
-
-
1
def custom_value_for(c)
-
field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
-
custom_values.detect {|v| v.custom_field_id == field_id }
-
end
-
-
1
def custom_field_value(c)
-
field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
-
custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
-
end
-
-
1
def validate_custom_field_values
-
1616
if new_record? || custom_field_values_changed?
-
1281
custom_field_values.each(&:validate_value)
-
end
-
end
-
-
1
def save_custom_field_values
-
2327
target_custom_values = []
-
2327
custom_field_values.each do |custom_field_value|
-
465
if custom_field_value.value.is_a?(Array)
-
custom_field_value.value.each do |v|
-
target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
-
target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
-
target_custom_values << target
-
end
-
else
-
1051
target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
-
465
target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
-
465
target.value = custom_field_value.value
-
465
target_custom_values << target
-
end
-
end
-
2327
self.custom_values = target_custom_values
-
2327
custom_values.each(&:save)
-
2327
@custom_field_values_changed = false
-
2327
true
-
end
-
-
1
def reset_custom_values!
-
@custom_field_values = nil
-
@custom_field_values_changed = true
-
end
-
-
1
module ClassMethods
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Acts
-
1
module Event
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def acts_as_event(options = {})
-
11
return if self.included_modules.include?(Redmine::Acts::Event::InstanceMethods)
-
11
default_options = { :datetime => :created_on,
-
:title => :title,
-
:description => :description,
-
:author => :author,
-
:url => {:controller => 'welcome'},
-
:type => self.name.underscore.dasherize }
-
-
11
cattr_accessor :event_options
-
11
self.event_options = default_options.merge(options)
-
11
send :include, Redmine::Acts::Event::InstanceMethods
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
11
base.extend ClassMethods
-
end
-
-
1
%w(datetime title description author type).each do |attr|
-
5
src = <<-END_SRC
-
def event_#{attr}
-
option = event_options[:#{attr}]
-
if option.is_a?(Proc)
-
option.call(self)
-
elsif option.is_a?(Symbol)
-
send(option)
-
else
-
option
-
end
-
end
-
END_SRC
-
5
class_eval src, __FILE__, __LINE__
-
end
-
-
1
def event_date
-
event_datetime.to_date
-
end
-
-
1
def event_url(options = {})
-
option = event_options[:url]
-
if option.is_a?(Proc)
-
option.call(self).merge(options)
-
elsif option.is_a?(Hash)
-
option.merge(options)
-
elsif option.is_a?(Symbol)
-
send(option).merge(options)
-
else
-
option
-
end
-
end
-
-
# Returns the mail adresses of users that should be notified
-
1
def recipients
-
notified = project.notified_users
-
notified.reject! {|user| !visible?(user)}
-
notified.collect(&:mail)
-
end
-
-
1
module ClassMethods
-
end
-
end
-
end
-
end
-
end
-
1
module ActiveRecord
-
1
module Acts #:nodoc:
-
1
module List #:nodoc:
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
# This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
-
# The class that has this specified needs to have a +position+ column defined as an integer on
-
# the mapped database table.
-
#
-
# Todo list example:
-
#
-
# class TodoList < ActiveRecord::Base
-
# has_many :todo_items, :order => "position"
-
# end
-
#
-
# class TodoItem < ActiveRecord::Base
-
# belongs_to :todo_list
-
# acts_as_list :scope => :todo_list
-
# end
-
#
-
# todo_list.first.move_to_bottom
-
# todo_list.last.move_higher
-
1
module ClassMethods
-
# Configuration options are:
-
#
-
# * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
-
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
-
# (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
-
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
-
# Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
-
1
def acts_as_list(options = {})
-
6
configuration = { :column => "position", :scope => "1 = 1" }
-
6
configuration.update(options) if options.is_a?(Hash)
-
-
6
configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
-
-
6
if configuration[:scope].is_a?(Symbol)
-
scope_condition_method = %(
-
def scope_condition
-
if #{configuration[:scope].to_s}.nil?
-
"#{configuration[:scope].to_s} IS NULL"
-
else
-
"#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
-
end
-
end
-
)
-
else
-
6
scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
-
end
-
-
class_eval <<-EOV
-
include ActiveRecord::Acts::List::InstanceMethods
-
-
def acts_as_list_class
-
::#{self.name}
-
end
-
-
def position_column
-
'#{configuration[:column]}'
-
end
-
-
#{scope_condition_method}
-
-
before_destroy :remove_from_list
-
before_create :add_to_list_bottom
-
6
EOV
-
end
-
end
-
-
# All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
-
# by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
-
# lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
-
# the first in the list of all chapters.
-
1
module InstanceMethods
-
# Insert the item at the given position (defaults to the top position of 1).
-
1
def insert_at(position = 1)
-
insert_at_position(position)
-
end
-
-
# Swap positions with the next lower item, if one exists.
-
1
def move_lower
-
return unless lower_item
-
-
acts_as_list_class.transaction do
-
lower_item.decrement_position
-
increment_position
-
end
-
end
-
-
# Swap positions with the next higher item, if one exists.
-
1
def move_higher
-
return unless higher_item
-
-
acts_as_list_class.transaction do
-
higher_item.increment_position
-
decrement_position
-
end
-
end
-
-
# Move to the bottom of the list. If the item is already in the list, the items below it have their
-
# position adjusted accordingly.
-
1
def move_to_bottom
-
return unless in_list?
-
acts_as_list_class.transaction do
-
decrement_positions_on_lower_items
-
assume_bottom_position
-
end
-
end
-
-
# Move to the top of the list. If the item is already in the list, the items above it have their
-
# position adjusted accordingly.
-
1
def move_to_top
-
return unless in_list?
-
acts_as_list_class.transaction do
-
increment_positions_on_higher_items
-
assume_top_position
-
end
-
end
-
-
# Move to the given position
-
1
def move_to=(pos)
-
case pos.to_s
-
when 'highest'
-
move_to_top
-
when 'higher'
-
move_higher
-
when 'lower'
-
move_lower
-
when 'lowest'
-
move_to_bottom
-
end
-
reset_positions_in_list
-
end
-
-
1
def reset_positions_in_list
-
acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i|
-
unless item.send(position_column) == (i + 1)
-
acts_as_list_class.update_all({position_column => (i + 1)}, {:id => item.id})
-
end
-
end
-
end
-
-
# Removes the item from the list.
-
1
def remove_from_list
-
if in_list?
-
decrement_positions_on_lower_items
-
update_attribute position_column, nil
-
end
-
end
-
-
# Increase the position of this item without adjusting the rest of the list.
-
1
def increment_position
-
return unless in_list?
-
update_attribute position_column, self.send(position_column).to_i + 1
-
end
-
-
# Decrease the position of this item without adjusting the rest of the list.
-
1
def decrement_position
-
return unless in_list?
-
update_attribute position_column, self.send(position_column).to_i - 1
-
end
-
-
# Return +true+ if this object is the first in the list.
-
1
def first?
-
return false unless in_list?
-
self.send(position_column) == 1
-
end
-
-
# Return +true+ if this object is the last in the list.
-
1
def last?
-
return false unless in_list?
-
self.send(position_column) == bottom_position_in_list
-
end
-
-
# Return the next higher item in the list.
-
1
def higher_item
-
return nil unless in_list?
-
acts_as_list_class.find(:first, :conditions =>
-
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
-
)
-
end
-
-
# Return the next lower item in the list.
-
1
def lower_item
-
return nil unless in_list?
-
acts_as_list_class.find(:first, :conditions =>
-
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
-
)
-
end
-
-
# Test if this record is in a list
-
1
def in_list?
-
!send(position_column).nil?
-
end
-
-
1
private
-
1
def add_to_list_top
-
increment_positions_on_all_items
-
end
-
-
1
def add_to_list_bottom
-
286
self[position_column] = bottom_position_in_list.to_i + 1
-
end
-
-
# Overwrite this method to define the scope of the list changes
-
1
def scope_condition() "1" end
-
-
# Returns the bottom position number in the list.
-
# bottom_position_in_list # => 2
-
1
def bottom_position_in_list(except = nil)
-
286
item = bottom_item(except)
-
286
item ? item.send(position_column) : 0
-
end
-
-
# Returns the bottom item
-
1
def bottom_item(except = nil)
-
286
conditions = scope_condition
-
286
conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
-
286
acts_as_list_class.where(conditions).reorder("#{position_column} DESC").first
-
end
-
-
# Forces item to assume the bottom position in the list.
-
1
def assume_bottom_position
-
update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
-
end
-
-
# Forces item to assume the top position in the list.
-
1
def assume_top_position
-
update_attribute(position_column, 1)
-
end
-
-
# This has the effect of moving all the higher items up one.
-
1
def decrement_positions_on_higher_items(position)
-
acts_as_list_class.update_all(
-
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
-
)
-
end
-
-
# This has the effect of moving all the lower items up one.
-
1
def decrement_positions_on_lower_items
-
return unless in_list?
-
acts_as_list_class.update_all(
-
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
-
)
-
end
-
-
# This has the effect of moving all the higher items down one.
-
1
def increment_positions_on_higher_items
-
return unless in_list?
-
acts_as_list_class.update_all(
-
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
-
)
-
end
-
-
# This has the effect of moving all the lower items down one.
-
1
def increment_positions_on_lower_items(position)
-
acts_as_list_class.update_all(
-
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
-
)
-
end
-
-
# Increments position (<tt>position_column</tt>) of all items in the list.
-
1
def increment_positions_on_all_items
-
acts_as_list_class.update_all(
-
"#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
-
)
-
end
-
-
1
def insert_at_position(position)
-
remove_from_list
-
increment_positions_on_lower_items(position)
-
self.update_attribute(position_column, position)
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Acts
-
1
module Searchable
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
# Options:
-
# * :columns - a column or an array of columns to search
-
# * :project_key - project foreign key (default to project_id)
-
# * :date_column - name of the datetime column (default to created_on)
-
# * :sort_order - name of the column used to sort results (default to :date_column or created_on)
-
# * :permission - permission required to search the model (default to :view_"objects")
-
1
def acts_as_searchable(options = {})
-
7
return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
-
-
7
cattr_accessor :searchable_options
-
7
self.searchable_options = options
-
-
7
if searchable_options[:columns].nil?
-
raise 'No searchable column defined.'
-
elsif !searchable_options[:columns].is_a?(Array)
-
1
searchable_options[:columns] = [] << searchable_options[:columns]
-
end
-
-
7
searchable_options[:project_key] ||= "#{table_name}.project_id"
-
7
searchable_options[:date_column] ||= "#{table_name}.created_on"
-
7
searchable_options[:order_column] ||= searchable_options[:date_column]
-
-
# Should we search custom fields on this model ?
-
7
searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
-
-
7
send :include, Redmine::Acts::Searchable::InstanceMethods
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
7
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
# Searches the model for the given tokens
-
# projects argument can be either nil (will search all projects), a project or an array of projects
-
# Returns the results and the results count
-
1
def search(tokens, projects=nil, options={})
-
if projects.is_a?(Array) && projects.empty?
-
# no results
-
return [[], 0]
-
end
-
-
# TODO: make user an argument
-
user = User.current
-
tokens = [] << tokens unless tokens.is_a?(Array)
-
projects = [] << projects unless projects.nil? || projects.is_a?(Array)
-
-
find_options = {:include => searchable_options[:include]}
-
find_options[:order] = "#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')
-
-
limit_options = {}
-
limit_options[:limit] = options[:limit] if options[:limit]
-
if options[:offset]
-
limit_options[:conditions] = "(#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
-
end
-
-
columns = searchable_options[:columns]
-
columns = columns[0..0] if options[:titles_only]
-
-
token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
-
-
if !options[:titles_only] && searchable_options[:search_custom_fields]
-
searchable_custom_field_ids = CustomField.find(:all,
-
:select => 'id',
-
:conditions => { :type => "#{self.name}CustomField",
-
:searchable => true }).collect(&:id)
-
if searchable_custom_field_ids.any?
-
custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" +
-
" WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" +
-
" AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))"
-
token_clauses << custom_field_sql
-
end
-
end
-
-
sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
-
-
find_options[:conditions] = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
-
-
scope = self
-
project_conditions = []
-
if searchable_options.has_key?(:permission)
-
project_conditions << Project.allowed_to_condition(user, searchable_options[:permission] || :view_project)
-
elsif respond_to?(:visible)
-
scope = scope.visible(user)
-
else
-
ActiveSupport::Deprecation.warn "acts_as_searchable with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option."
-
project_conditions << Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym)
-
end
-
# TODO: use visible scope options instead
-
project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
-
project_conditions = project_conditions.empty? ? nil : project_conditions.join(' AND ')
-
-
results = []
-
results_count = 0
-
-
scope = scope.scoped({:conditions => project_conditions}).scoped(find_options)
-
results_count = scope.count(:all)
-
results = scope.find(:all, limit_options)
-
-
[results, results_count]
-
end
-
end
-
end
-
end
-
end
-
end
-
1
module ActiveRecord
-
1
module Acts
-
1
module Tree
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
# Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
-
# association. This requires that you have a foreign key column, which by default is called +parent_id+.
-
#
-
# class Category < ActiveRecord::Base
-
# acts_as_tree :order => "name"
-
# end
-
#
-
# Example:
-
# root
-
# \_ child1
-
# \_ subchild1
-
# \_ subchild2
-
#
-
# root = Category.create("name" => "root")
-
# child1 = root.children.create("name" => "child1")
-
# subchild1 = child1.children.create("name" => "subchild1")
-
#
-
# root.parent # => nil
-
# child1.parent # => root
-
# root.children # => [child1]
-
# root.children.first.children.first # => subchild1
-
#
-
# In addition to the parent and children associations, the following instance methods are added to the class
-
# after calling <tt>acts_as_tree</tt>:
-
# * <tt>siblings</tt> - Returns all the children of the parent, excluding the current node (<tt>[subchild2]</tt> when called on <tt>subchild1</tt>)
-
# * <tt>self_and_siblings</tt> - Returns all the children of the parent, including the current node (<tt>[subchild1, subchild2]</tt> when called on <tt>subchild1</tt>)
-
# * <tt>ancestors</tt> - Returns all the ancestors of the current node (<tt>[child1, root]</tt> when called on <tt>subchild2</tt>)
-
# * <tt>root</tt> - Returns the root of the current node (<tt>root</tt> when called on <tt>subchild2</tt>)
-
1
module ClassMethods
-
# Configuration options are:
-
#
-
# * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
-
# * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
-
# * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
-
1
def acts_as_tree(options = {})
-
4
configuration = { :foreign_key => "parent_id", :dependent => :destroy, :order => nil, :counter_cache => nil }
-
4
configuration.update(options) if options.is_a?(Hash)
-
-
4
belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
-
4
has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => configuration[:dependent]
-
-
class_eval <<-EOV
-
include ActiveRecord::Acts::Tree::InstanceMethods
-
-
def self.roots
-
find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
-
end
-
-
def self.root
-
find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
-
end
-
4
EOV
-
end
-
end
-
-
1
module InstanceMethods
-
# Returns list of ancestors, starting from parent until root.
-
#
-
# subchild1.ancestors # => [child1, root]
-
1
def ancestors
-
4
node, nodes = self, []
-
4
nodes << node = node.parent while node.parent
-
4
nodes
-
end
-
-
# Returns list of descendants.
-
#
-
# root.descendants # => [child1, subchild1, subchild2]
-
1
def descendants(depth=nil)
-
2
depth ||= 0
-
2
result = children.dup
-
2
unless depth == 1
-
2
result += children.collect {|child| child.descendants(depth-1)}.flatten
-
end
-
2
result
-
end
-
-
# Returns list of descendants and a reference to the current node.
-
#
-
# root.self_and_descendants # => [root, child1, subchild1, subchild2]
-
1
def self_and_descendants(depth=nil)
-
2
[self] + descendants(depth)
-
end
-
-
# Returns the root node of the tree.
-
1
def root
-
node = self
-
node = node.parent while node.parent
-
node
-
end
-
-
# Returns all siblings of the current node.
-
#
-
# subchild1.siblings # => [subchild2]
-
1
def siblings
-
self_and_siblings - [self]
-
end
-
-
# Returns all siblings and a reference to the current node.
-
#
-
# subchild1.self_and_siblings # => [subchild1, subchild2]
-
1
def self_and_siblings
-
parent ? parent.children : self.class.roots
-
end
-
end
-
end
-
end
-
end
-
# Copyright (c) 2005 Rick Olson
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining
-
# a copy of this software and associated documentation files (the
-
# "Software"), to deal in the Software without restriction, including
-
# without limitation the rights to use, copy, modify, merge, publish,
-
# distribute, sublicense, and/or sell copies of the Software, and to
-
# permit persons to whom the Software is furnished to do so, subject to
-
# the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be
-
# included in all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-
1
module ActiveRecord #:nodoc:
-
1
module Acts #:nodoc:
-
# Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
-
# versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
-
# column is present as well.
-
#
-
# The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
-
# your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
-
#
-
# class Page < ActiveRecord::Base
-
# # assumes pages_versions table
-
# acts_as_versioned
-
# end
-
#
-
# Example:
-
#
-
# page = Page.create(:title => 'hello world!')
-
# page.version # => 1
-
#
-
# page.title = 'hello world'
-
# page.save
-
# page.version # => 2
-
# page.versions.size # => 2
-
#
-
# page.revert_to(1) # using version number
-
# page.title # => 'hello world!'
-
#
-
# page.revert_to(page.versions.last) # using versioned instance
-
# page.title # => 'hello world'
-
#
-
# page.versions.earliest # efficient query to find the first version
-
# page.versions.latest # efficient query to find the most recently created version
-
#
-
#
-
# Simple Queries to page between versions
-
#
-
# page.versions.before(version)
-
# page.versions.after(version)
-
#
-
# Access the previous/next versions from the versioned model itself
-
#
-
# version = page.versions.latest
-
# version.previous # go back one version
-
# version.next # go forward one version
-
#
-
# See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
-
1
module Versioned
-
1
CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
-
1
def self.included(base) # :nodoc:
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
# == Configuration options
-
#
-
# * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
-
# * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
-
# * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
-
# * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
-
# * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
-
# * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
-
# * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
-
# * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
-
# For finer control, pass either a Proc or modify Model#version_condition_met?
-
#
-
# acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
-
#
-
# or...
-
#
-
# class Auction
-
# def version_condition_met? # totally bypasses the <tt>:if</tt> option
-
# !expired?
-
# end
-
# end
-
#
-
# * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
-
# either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
-
# Use this instead if you want to write your own attribute setters (and ignore if_changed):
-
#
-
# def name=(new_name)
-
# write_changed_attribute :name, new_name
-
# end
-
#
-
# * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
-
# to create an anonymous mixin:
-
#
-
# class Auction
-
# acts_as_versioned do
-
# def started?
-
# !started_at.nil?
-
# end
-
# end
-
# end
-
#
-
# or...
-
#
-
# module AuctionExtension
-
# def started?
-
# !started_at.nil?
-
# end
-
# end
-
# class Auction
-
# acts_as_versioned :extend => AuctionExtension
-
# end
-
#
-
# Example code:
-
#
-
# @auction = Auction.find(1)
-
# @auction.started?
-
# @auction.versions.first.started?
-
#
-
# == Database Schema
-
#
-
# The model that you're versioning needs to have a 'version' attribute. The model is versioned
-
# into a table called #{model}_versions where the model name is singlular. The _versions table should
-
# contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
-
#
-
# A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
-
# then that field is reflected in the versioned model as 'versioned_type' by default.
-
#
-
# Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
-
# method, perfect for a migration. It will also create the version column if the main model does not already have it.
-
#
-
# class AddVersions < ActiveRecord::Migration
-
# def self.up
-
# # create_versioned_table takes the same options hash
-
# # that create_table does
-
# Post.create_versioned_table
-
# end
-
#
-
# def self.down
-
# Post.drop_versioned_table
-
# end
-
# end
-
#
-
# == Changing What Fields Are Versioned
-
#
-
# By default, acts_as_versioned will version all but these fields:
-
#
-
# [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
-
#
-
# You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
-
#
-
# class Post < ActiveRecord::Base
-
# acts_as_versioned
-
# self.non_versioned_columns << 'comments_count'
-
# end
-
#
-
1
def acts_as_versioned(options = {}, &extension)
-
# don't allow multiple calls
-
1
return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
-
-
1
send :include, ActiveRecord::Acts::Versioned::ActMethods
-
-
1
cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
-
:version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
-
:version_association_options
-
-
# legacy
-
1
alias_method :non_versioned_fields, :non_versioned_columns
-
1
alias_method :non_versioned_fields=, :non_versioned_columns=
-
-
1
class << self
-
1
alias_method :non_versioned_fields, :non_versioned_columns
-
1
alias_method :non_versioned_fields=, :non_versioned_columns=
-
end
-
-
1
send :attr_accessor, :altered_attributes
-
-
1
self.versioned_class_name = options[:class_name] || "Version"
-
1
self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
-
1
self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
-
1
self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
-
1
self.version_column = options[:version_column] || 'version'
-
1
self.version_sequence_name = options[:sequence_name]
-
1
self.max_version_limit = options[:limit].to_i
-
1
self.version_condition = options[:if] || true
-
1
self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
-
1
self.version_association_options = {
-
:class_name => "#{self.to_s}::#{versioned_class_name}",
-
:foreign_key => versioned_foreign_key,
-
:dependent => :delete_all
-
}.merge(options[:association_options] || {})
-
-
1
if block_given?
-
extension_module_name = "#{versioned_class_name}Extension"
-
silence_warnings do
-
self.const_set(extension_module_name, Module.new(&extension))
-
end
-
-
options[:extend] = self.const_get(extension_module_name)
-
end
-
-
1
class_eval do
-
1
has_many :versions, version_association_options do
-
# finds earliest version of this record
-
1
def earliest
-
@earliest ||= find(:first, :order => 'version')
-
end
-
-
# find latest version of this record
-
1
def latest
-
@latest ||= find(:first, :order => 'version desc')
-
end
-
end
-
1
before_save :set_new_version
-
1
after_create :save_version_on_create
-
1
after_update :save_version
-
1
after_save :clear_old_versions
-
1
after_save :clear_altered_attributes
-
-
1
unless options[:if_changed].nil?
-
self.track_altered_attributes = true
-
options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
-
options[:if_changed].each do |attr_name|
-
define_method("#{attr_name}=") do |value|
-
write_changed_attribute attr_name, value
-
end
-
end
-
end
-
-
1
include options[:extend] if options[:extend].is_a?(Module)
-
end
-
-
# create the dynamic versioned model
-
1
const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
-
1
def self.reloadable? ; false ; end
-
# find first version before the given version
-
1
def self.before(version)
-
find :first, :order => 'version desc',
-
:conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
-
end
-
-
# find first version after the given version.
-
1
def self.after(version)
-
find :first, :order => 'version',
-
:conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
-
end
-
-
1
def previous
-
self.class.before(self)
-
end
-
-
1
def next
-
self.class.after(self)
-
end
-
-
1
def versions_count
-
page.version
-
end
-
end
-
-
1
versioned_class.cattr_accessor :original_class
-
1
versioned_class.original_class = self
-
1
versioned_class.table_name = versioned_table_name
-
1
versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
-
:class_name => "::#{self.to_s}",
-
:foreign_key => versioned_foreign_key
-
1
versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
-
1
versioned_class.set_sequence_name version_sequence_name if version_sequence_name
-
end
-
end
-
-
1
module ActMethods
-
1
def self.included(base) # :nodoc:
-
1
base.extend ClassMethods
-
end
-
-
# Finds a specific version of this record
-
1
def find_version(version = nil)
-
self.class.find_version(id, version)
-
end
-
-
# Saves a version of the model if applicable
-
1
def save_version
-
save_version_on_create if save_version?
-
end
-
-
# Saves a version of the model in the versioned table. This is called in the after_save callback by default
-
1
def save_version_on_create
-
6
rev = self.class.versioned_class.new
-
6
self.clone_versioned_model(self, rev)
-
6
rev.version = send(self.class.version_column)
-
6
rev.send("#{self.class.versioned_foreign_key}=", self.id)
-
6
rev.save
-
end
-
-
# Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
-
# Override this method to set your own criteria for clearing old versions.
-
1
def clear_old_versions
-
6
return if self.class.max_version_limit == 0
-
excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
-
if excess_baggage > 0
-
sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
-
self.class.versioned_class.connection.execute sql
-
end
-
end
-
-
1
def versions_count
-
version
-
end
-
-
# Reverts a model to a given version. Takes either a version number or an instance of the versioned model
-
1
def revert_to(version)
-
if version.is_a?(self.class.versioned_class)
-
return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
-
else
-
return false unless version = versions.find_by_version(version)
-
end
-
self.clone_versioned_model(version, self)
-
self.send("#{self.class.version_column}=", version.version)
-
true
-
end
-
-
# Reverts a model to a given version and saves the model.
-
# Takes either a version number or an instance of the versioned model
-
1
def revert_to!(version)
-
revert_to(version) ? save_without_revision : false
-
end
-
-
# Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
-
1
def save_without_revision
-
save_without_revision!
-
true
-
rescue
-
false
-
end
-
-
1
def save_without_revision!
-
without_locking do
-
without_revision do
-
save!
-
end
-
end
-
end
-
-
# Returns an array of attribute keys that are versioned. See non_versioned_columns
-
1
def versioned_attributes
-
48
self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
-
end
-
-
# If called with no parameters, gets whether the current model has changed and needs to be versioned.
-
# If called with a single parameter, gets whether the parameter has changed.
-
1
def changed?(attr_name = nil)
-
attr_name.nil? ?
-
(!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
-
(altered_attributes && altered_attributes.include?(attr_name.to_s))
-
end
-
-
# keep old dirty? method
-
1
alias_method :dirty?, :changed?
-
-
# Clones a model. Used when saving a new version or reverting a model's version.
-
1
def clone_versioned_model(orig_model, new_model)
-
6
self.versioned_attributes.each do |key|
-
30
new_model.send("#{key}=", orig_model.send(key)) if orig_model.respond_to?(key)
-
end
-
-
6
if self.class.columns_hash.include?(self.class.inheritance_column)
-
if orig_model.is_a?(self.class.versioned_class)
-
new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
-
elsif new_model.is_a?(self.class.versioned_class)
-
new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
-
end
-
end
-
end
-
-
# Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
-
1
def save_version?
-
version_condition_met? && changed?
-
end
-
-
# Checks condition set in the :if option to check whether a revision should be created or not. Override this for
-
# custom version condition checking.
-
1
def version_condition_met?
-
case
-
when version_condition.is_a?(Symbol)
-
send(version_condition)
-
when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
-
version_condition.call(self)
-
else
-
version_condition
-
end
-
end
-
-
# Executes the block with the versioning callbacks disabled.
-
#
-
# @foo.without_revision do
-
# @foo.save
-
# end
-
#
-
1
def without_revision(&block)
-
self.class.without_revision(&block)
-
end
-
-
# Turns off optimistic locking for the duration of the block
-
#
-
# @foo.without_locking do
-
# @foo.save
-
# end
-
#
-
1
def without_locking(&block)
-
self.class.without_locking(&block)
-
end
-
-
1
def empty_callback() end #:nodoc:
-
-
1
protected
-
# sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
-
1
def set_new_version
-
6
self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
-
end
-
-
# Gets the next available version for the current record, or 1 for a new record
-
1
def next_version
-
6
return 1 if new_record?
-
(versions.calculate(:max, :version) || 0) + 1
-
end
-
-
# clears current changed attributes. Called after save.
-
1
def clear_altered_attributes
-
6
self.altered_attributes = []
-
end
-
-
1
def write_changed_attribute(attr_name, attr_value)
-
# Convert to db type for comparison. Avoids failing Float<=>String comparisons.
-
attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
-
(self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
-
write_attribute(attr_name, attr_value_for_db)
-
end
-
-
1
module ClassMethods
-
# Finds a specific version of a specific row of this model
-
1
def find_version(id, version = nil)
-
return find(id) unless version
-
-
conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
-
options = { :conditions => conditions, :limit => 1 }
-
-
if result = find_versions(id, options).first
-
result
-
else
-
raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
-
end
-
end
-
-
# Finds versions of a specific model. Takes an options hash like <tt>find</tt>
-
1
def find_versions(id, options = {})
-
versioned_class.find :all, {
-
:conditions => ["#{versioned_foreign_key} = ?", id],
-
:order => 'version' }.merge(options)
-
end
-
-
# Returns an array of columns that are versioned. See non_versioned_columns
-
1
def versioned_columns
-
self.columns.select { |c| !non_versioned_columns.include?(c.name) }
-
end
-
-
# Returns an instance of the dynamic versioned model
-
1
def versioned_class
-
10
const_get versioned_class_name
-
end
-
-
# Rake migration task to create the versioned table using options passed to acts_as_versioned
-
1
def create_versioned_table(create_table_options = {})
-
# create version column in main table if it does not exist
-
if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
-
self.connection.add_column table_name, :version, :integer
-
end
-
-
self.connection.create_table(versioned_table_name, create_table_options) do |t|
-
t.column versioned_foreign_key, :integer
-
t.column :version, :integer
-
end
-
-
updated_col = nil
-
self.versioned_columns.each do |col|
-
updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
-
self.connection.add_column versioned_table_name, col.name, col.type,
-
:limit => col.limit,
-
:default => col.default,
-
:scale => col.scale,
-
:precision => col.precision
-
end
-
-
if type_col = self.columns_hash[inheritance_column]
-
self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
-
:limit => type_col.limit,
-
:default => type_col.default,
-
:scale => type_col.scale,
-
:precision => type_col.precision
-
end
-
-
if updated_col.nil?
-
self.connection.add_column versioned_table_name, :updated_at, :timestamp
-
end
-
end
-
-
# Rake migration task to drop the versioned table
-
1
def drop_versioned_table
-
self.connection.drop_table versioned_table_name
-
end
-
-
# Executes the block with the versioning callbacks disabled.
-
#
-
# Foo.without_revision do
-
# @foo.save
-
# end
-
#
-
1
def without_revision(&block)
-
class_eval do
-
CALLBACKS.each do |attr_name|
-
alias_method "orig_#{attr_name}".to_sym, attr_name
-
alias_method attr_name, :empty_callback
-
end
-
end
-
block.call
-
ensure
-
class_eval do
-
CALLBACKS.each do |attr_name|
-
alias_method attr_name, "orig_#{attr_name}".to_sym
-
end
-
end
-
end
-
-
# Turns off optimistic locking for the duration of the block
-
#
-
# Foo.without_locking do
-
# @foo.save
-
# end
-
#
-
1
def without_locking(&block)
-
current = ActiveRecord::Base.lock_optimistically
-
ActiveRecord::Base.lock_optimistically = false if current
-
result = block.call
-
ActiveRecord::Base.lock_optimistically = true if current
-
result
-
end
-
end
-
end
-
end
-
end
-
end
-
-
1
ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
-
# ActsAsWatchable
-
1
module Redmine
-
1
module Acts
-
1
module Watchable
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def acts_as_watchable(options = {})
-
6
return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods)
-
6
class_eval do
-
6
has_many :watchers, :as => :watchable, :dependent => :delete_all
-
6
has_many :watcher_users, :through => :watchers, :source => :user, :validate => false
-
-
6
scope :watched_by, lambda { |user_id|
-
{ :include => :watchers,
-
:conditions => ["#{Watcher.table_name}.user_id = ?", user_id] }
-
}
-
6
attr_protected :watcher_ids, :watcher_user_ids
-
end
-
6
send :include, Redmine::Acts::Watchable::InstanceMethods
-
6
alias_method_chain :watcher_user_ids=, :uniq_ids
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
6
base.extend ClassMethods
-
end
-
-
# Returns an array of users that are proposed as watchers
-
1
def addable_watcher_users
-
users = self.project.users.sort - self.watcher_users
-
if respond_to?(:visible?)
-
users.reject! {|user| !visible?(user)}
-
end
-
users
-
end
-
-
# Adds user as a watcher
-
1
def add_watcher(user)
-
self.watchers << Watcher.new(:user => user)
-
end
-
-
# Removes user from the watchers list
-
1
def remove_watcher(user)
-
return nil unless user && user.is_a?(User)
-
Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
-
end
-
-
# Adds/removes watcher
-
1
def set_watcher(user, watching=true)
-
watching ? add_watcher(user) : remove_watcher(user)
-
end
-
-
# Overrides watcher_user_ids= to make user_ids uniq
-
1
def watcher_user_ids_with_uniq_ids=(user_ids)
-
2401
if user_ids.is_a?(Array)
-
2401
user_ids = user_ids.uniq
-
end
-
2401
send :watcher_user_ids_without_uniq_ids=, user_ids
-
end
-
-
# Returns true if object is watched by +user+
-
1
def watched_by?(user)
-
12
!!(user && self.watcher_user_ids.detect {|uid| uid == user.id })
-
end
-
-
1
def notified_watchers
-
1003
notified = watcher_users.active
-
1003
notified.reject! {|user| user.mail.blank? || user.mail_notification == 'none'}
-
1003
if respond_to?(:visible?)
-
1003
notified.reject! {|user| !visible?(user)}
-
end
-
1003
notified
-
end
-
-
# Returns an array of watchers' email addresses
-
1
def watcher_recipients
-
862
notified_watchers.collect(&:mail)
-
end
-
-
1
module ClassMethods; end
-
end
-
end
-
end
-
end
-
1
require 'awesome_nested_set/awesome_nested_set'
-
1
ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
-
-
1
if defined?(ActionView)
-
1
require 'awesome_nested_set/helper'
-
1
ActionView::Base.send :include, CollectiveIdea::Acts::NestedSet::Helper
-
end
-
1
module CollectiveIdea #:nodoc:
-
1
module Acts #:nodoc:
-
1
module NestedSet #:nodoc:
-
-
# This acts provides Nested Set functionality. Nested Set is a smart way to implement
-
# an _ordered_ tree, with the added feature that you can select the children and all of their
-
# descendants with a single query. The drawback is that insertion or move need some complex
-
# sql queries. But everything is done here by this module!
-
#
-
# Nested sets are appropriate each time you want either an orderd tree (menus,
-
# commercial categories) or an efficient way of querying big trees (threaded posts).
-
#
-
# == API
-
#
-
# Methods names are aligned with acts_as_tree as much as possible to make replacment from one
-
# by another easier.
-
#
-
# item.children.create(:name => "child1")
-
#
-
-
# Configuration options are:
-
#
-
# * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
-
# * +:left_column+ - column name for left boundry data, default "lft"
-
# * +:right_column+ - column name for right boundry data, default "rgt"
-
# * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
-
# (if it hasn't been already) and use that as the foreign key restriction. You
-
# can also pass an array to scope by multiple attributes.
-
# Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
-
# * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
-
# child objects are destroyed alongside this object by calling their destroy
-
# method. If set to :delete_all (default), all the child objects are deleted
-
# without calling their destroy method.
-
# * +:counter_cache+ adds a counter cache for the number of children.
-
# defaults to false.
-
# Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
-
#
-
# See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
-
# CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
-
# to acts_as_nested_set models
-
1
def acts_as_nested_set(options = {})
-
2
options = {
-
:parent_column => 'parent_id',
-
:left_column => 'lft',
-
:right_column => 'rgt',
-
:dependent => :delete_all, # or :destroy
-
:counter_cache => false,
-
:order => 'id'
-
}.merge(options)
-
-
2
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
-
options[:scope] = "#{options[:scope]}_id".intern
-
end
-
-
2
class_attribute :acts_as_nested_set_options
-
2
self.acts_as_nested_set_options = options
-
-
2
include CollectiveIdea::Acts::NestedSet::Model
-
2
include Columns
-
2
extend Columns
-
-
2
belongs_to :parent, :class_name => self.base_class.to_s,
-
:foreign_key => parent_column_name,
-
:counter_cache => options[:counter_cache],
-
:inverse_of => :children
-
2
has_many :children, :class_name => self.base_class.to_s,
-
:foreign_key => parent_column_name, :order => left_column_name,
-
:inverse_of => :parent,
-
:before_add => options[:before_add],
-
:after_add => options[:after_add],
-
:before_remove => options[:before_remove],
-
:after_remove => options[:after_remove]
-
-
2
attr_accessor :skip_before_destroy
-
-
2
before_create :set_default_left_and_right
-
2
before_save :store_new_parent
-
2
after_save :move_to_new_parent
-
2
before_destroy :destroy_descendants
-
-
# no assignment to structure fields
-
2
[left_column_name, right_column_name].each do |column|
-
4
module_eval <<-"end_eval", __FILE__, __LINE__
-
def #{column}=(x)
-
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
-
end
-
end_eval
-
end
-
-
2
define_model_callbacks :move
-
end
-
-
1
module Model
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
# Returns the first root
-
1
def root
-
roots.first
-
end
-
-
1
def roots
-
32
where(parent_column_name => nil).order(quoted_left_column_name)
-
end
-
-
1
def leaves
-
1
where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
-
end
-
-
1
def valid?
-
left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
-
end
-
-
1
def left_and_rights_valid?
-
joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
-
"#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
-
where(
-
"#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
-
"#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
-
"#{quoted_table_name}.#{quoted_left_column_name} >= " +
-
"#{quoted_table_name}.#{quoted_right_column_name} OR " +
-
"(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
-
"(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
-
"#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
-
).count == 0
-
end
-
-
1
def no_duplicates_for_columns?
-
scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
-
connection.quote_column_name(c)
-
end.push(nil).join(", ")
-
[quoted_left_column_name, quoted_right_column_name].all? do |column|
-
# No duplicates
-
select("#{scope_string}#{column}, COUNT(#{column})").
-
group("#{scope_string}#{column}").
-
having("COUNT(#{column}) > 1").
-
first.nil?
-
end
-
end
-
-
# Wrapper for each_root_valid? that can deal with scope.
-
1
def all_roots_valid?
-
if acts_as_nested_set_options[:scope]
-
roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
-
each_root_valid?(grouped_roots)
-
end
-
else
-
each_root_valid?(roots)
-
end
-
end
-
-
1
def each_root_valid?(roots_to_validate)
-
left = right = 0
-
roots_to_validate.all? do |root|
-
(root.left > left && root.right > right).tap do
-
left = root.left
-
right = root.right
-
end
-
end
-
end
-
-
# Rebuilds the left & rights if unset or invalid.
-
# Also very useful for converting from acts_as_tree.
-
1
def rebuild!(validate_nodes = true)
-
# Don't rebuild a valid tree.
-
return true if valid?
-
-
scope = lambda{|node|}
-
if acts_as_nested_set_options[:scope]
-
scope = lambda{|node|
-
scope_column_names.inject(""){|str, column_name|
-
str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
-
}
-
}
-
end
-
indices = {}
-
-
set_left_and_rights = lambda do |node|
-
# set left
-
node[left_column_name] = indices[scope.call(node)] += 1
-
# find
-
where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).order(acts_as_nested_set_options[:order]).each{|n| set_left_and_rights.call(n) }
-
# set right
-
node[right_column_name] = indices[scope.call(node)] += 1
-
node.save!(:validate => validate_nodes)
-
end
-
-
# Find root node(s)
-
root_nodes = where("#{quoted_parent_column_name} IS NULL").order(acts_as_nested_set_options[:order]).each do |root_node|
-
# setup index for this scope
-
indices[scope.call(root_node)] ||= 0
-
set_left_and_rights.call(root_node)
-
end
-
end
-
-
# Iterates over tree elements and determines the current level in the tree.
-
# Only accepts default ordering, odering by an other column than lft
-
# does not work. This method is much more efficent than calling level
-
# because it doesn't require any additional database queries.
-
#
-
# Example:
-
# Category.each_with_level(Category.root.self_and_descendants) do |o, level|
-
#
-
1
def each_with_level(objects)
-
path = [nil]
-
objects.each do |o|
-
if o.parent_id != path.last
-
# we are on a new level, did we decent or ascent?
-
if path.include?(o.parent_id)
-
# remove wrong wrong tailing paths elements
-
path.pop while path.last != o.parent_id
-
else
-
path << o.parent_id
-
end
-
end
-
yield(o, path.length - 1)
-
end
-
end
-
end
-
-
# Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
-
#
-
# category.self_and_descendants.count
-
# category.ancestors.find(:all, :conditions => "name like '%foo%'")
-
-
# Value of the parent column
-
1
def parent_id
-
11947
self[parent_column_name]
-
end
-
-
# Value of the left column
-
1
def left
-
16386
self[left_column_name]
-
end
-
-
# Value of the right column
-
1
def right
-
14002
self[right_column_name]
-
end
-
-
# Returns true if this is a root node.
-
1
def root?
-
1326
parent_id.nil?
-
end
-
-
1
def leaf?
-
906
new_record? || (right - left == 1)
-
end
-
-
# Returns true is this is a child node
-
1
def child?
-
726
!parent_id.nil?
-
end
-
-
# Returns root
-
1
def root
-
280
self_and_ancestors.where(parent_column_name => nil).first
-
end
-
-
# Returns the array of all parents and self
-
1
def self_and_ancestors
-
529
nested_set_scope.where([
-
"#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
-
])
-
end
-
-
# Returns an array of all parents
-
1
def ancestors
-
242
without_self self_and_ancestors
-
end
-
-
# Returns the array of all children of the parent, including self
-
1
def self_and_siblings
-
300
nested_set_scope.where(parent_column_name => parent_id)
-
end
-
-
# Returns the array of all children of the parent, except self
-
1
def siblings
-
300
without_self self_and_siblings
-
end
-
-
# Returns a set of all of its nested children which do not have children
-
1
def leaves
-
3898
descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
-
end
-
-
# Returns the level of this object in the tree
-
# root level is 0
-
1
def level
-
parent_id.nil? ? 0 : ancestors.count
-
end
-
-
# Returns a set of itself and all of its nested children
-
1
def self_and_descendants
-
5996
nested_set_scope.where([
-
"#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
-
])
-
end
-
-
# Returns a set of all of its children and nested children
-
1
def descendants
-
5794
without_self self_and_descendants
-
end
-
-
1
def is_descendant_of?(other)
-
1429
other.left < self.left && self.left < other.right && same_scope?(other)
-
end
-
-
1
def is_or_is_descendant_of?(other)
-
other.left <= self.left && self.left < other.right && same_scope?(other)
-
end
-
-
1
def is_ancestor_of?(other)
-
89
self.left < other.left && other.left < self.right && same_scope?(other)
-
end
-
-
1
def is_or_is_ancestor_of?(other)
-
self.left <= other.left && other.left < self.right && same_scope?(other)
-
end
-
-
# Check if other model is in the same scope
-
1
def same_scope?(other)
-
792
Array(acts_as_nested_set_options[:scope]).all? do |attr|
-
270
self.send(attr) == other.send(attr)
-
end
-
end
-
-
# Find the first sibling to the left
-
1
def left_sibling
-
siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
-
order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
-
end
-
-
# Find the first sibling to the right
-
1
def right_sibling
-
151
siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
-
end
-
-
# Shorthand method for finding the left sibling and moving to the left of it.
-
1
def move_left
-
move_to_left_of left_sibling
-
end
-
-
# Shorthand method for finding the right sibling and moving to the right of it.
-
1
def move_right
-
move_to_right_of right_sibling
-
end
-
-
# Move the node to the left of another node (you can pass id only)
-
1
def move_to_left_of(node)
-
move_to node, :left
-
end
-
-
# Move the node to the left of another node (you can pass id only)
-
1
def move_to_right_of(node)
-
34
move_to node, :right
-
end
-
-
# Move the node to the child of another node (you can pass id only)
-
1
def move_to_child_of(node)
-
147
move_to node, :child
-
end
-
-
# Move the node to root nodes
-
1
def move_to_root
-
move_to nil, :root
-
end
-
-
1
def move_possible?(target)
-
self != target && # Can't target self
-
310
same_scope?(target) && # can't be in different scopes
-
# !(left..right).include?(target.left..target.right) # this needs tested more
-
# detect impossible move
-
310
!((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
-
end
-
-
1
def to_text
-
self_and_descendants.map do |node|
-
"#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
-
end.join("\n")
-
end
-
-
1
protected
-
-
1
def without_self(scope)
-
6336
scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
-
end
-
-
# All nested set queries should use this nested_set_scope, which performs finds on
-
# the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
-
# declaration.
-
1
def nested_set_scope(options = {})
-
11117
options = {:order => "#{self.class.quoted_table_name}.#{quoted_left_column_name}"}.merge(options)
-
11117
scopes = Array(acts_as_nested_set_options[:scope])
-
options[:conditions] = scopes.inject({}) do |conditions,attr|
-
10602
conditions.merge attr => self[attr]
-
11117
end unless scopes.empty?
-
11117
self.class.base_class.scoped options
-
end
-
-
1
def store_new_parent
-
1801
@move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
-
1801
true # force callback to return true
-
end
-
-
1
def move_to_new_parent
-
1801
if @move_to_new_parent_id.nil?
-
move_to_root
-
1801
elsif @move_to_new_parent_id
-
move_to_child_of(@move_to_new_parent_id)
-
end
-
end
-
-
# on creation, set automatically lft and rgt to the end of the tree
-
1
def set_default_left_and_right
-
1756
highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true )
-
1756
maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
-
# adds the new node to the right of all existing nodes
-
1756
self[left_column_name] = maxright + 1
-
1756
self[right_column_name] = maxright + 2
-
end
-
-
1
def in_tenacious_transaction(&block)
-
1344
retry_count = 0
-
1344
begin
-
1344
transaction(&block)
-
rescue ActiveRecord::StatementInvalid => error
-
raise unless connection.open_transactions.zero?
-
raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
-
raise unless retry_count < 10
-
retry_count += 1
-
logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
-
sleep(rand(retry_count)*0.1) # Aloha protocol
-
retry
-
end
-
end
-
-
# Prunes a branch off of the tree, shifting all of the elements on the right
-
# back to the left so the counts still work.
-
1
def destroy_descendants
-
1163
return if right.nil? || left.nil? || skip_before_destroy
-
-
1163
in_tenacious_transaction do
-
1163
reload_nested_set
-
# select the rows in the model that extend past the deletion point and apply a lock
-
1163
self.class.base_class.find(:all,
-
:select => "id",
-
:conditions => ["#{quoted_left_column_name} >= ?", left],
-
:lock => true
-
)
-
-
1163
if acts_as_nested_set_options[:dependent] == :destroy
-
1163
descendants.each do |model|
-
model.skip_before_destroy = true
-
model.destroy
-
end
-
else
-
nested_set_scope.delete_all(
-
["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
-
left, right]
-
)
-
end
-
-
# update lefts and rights for remaining nodes
-
1163
diff = right - left + 1
-
1163
nested_set_scope.update_all(
-
["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
-
["#{quoted_left_column_name} > ?", right]
-
)
-
1163
nested_set_scope.update_all(
-
["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
-
["#{quoted_right_column_name} > ?", right]
-
)
-
-
1163
reload
-
# Don't allow multiple calls to destroy to corrupt the set
-
1163
self.skip_before_destroy = true
-
end
-
end
-
-
# reload left, right, and parent
-
1
def reload_nested_set
-
reload(
-
:select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
-
1828
:lock => true
-
)
-
end
-
-
1
def move_to(target, position)
-
181
raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
-
181
run_callbacks :move do
-
181
in_tenacious_transaction do
-
181
if target.is_a? self.class.base_class
-
148
target.reload_nested_set
-
elsif position != :root
-
# load object if node is not an object
-
33
target = nested_set_scope.find(target)
-
end
-
181
self.reload_nested_set
-
-
181
unless position == :root || move_possible?(target)
-
raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
-
end
-
-
181
bound = case position
-
147
when :child; target[right_column_name]
-
when :left; target[left_column_name]
-
34
when :right; target[right_column_name] + 1
-
when :root; 1
-
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
-
end
-
-
181
if bound > self[right_column_name]
-
21
bound = bound - 1
-
21
other_bound = self[right_column_name] + 1
-
else
-
160
other_bound = self[left_column_name] - 1
-
end
-
-
# there would be no change
-
181
return if bound == self[right_column_name] || bound == self[left_column_name]
-
-
# we have defined the boundaries of two non-overlapping intervals,
-
# so sorting puts both the intervals and their boundaries in order
-
168
a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
-
-
# select the rows in the model between a and d, and apply a lock
-
168
self.class.base_class.select('id').lock(true).where(
-
["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
-
)
-
-
168
new_parent = case position
-
147
when :child; target.id
-
when :root; nil
-
21
else target[parent_column_name]
-
end
-
-
168
self.nested_set_scope.update_all([
-
"#{quoted_left_column_name} = CASE " +
-
"WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
-
"THEN #{quoted_left_column_name} + :d - :b " +
-
"WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
-
"THEN #{quoted_left_column_name} + :a - :c " +
-
"ELSE #{quoted_left_column_name} END, " +
-
"#{quoted_right_column_name} = CASE " +
-
"WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
-
"THEN #{quoted_right_column_name} + :d - :b " +
-
"WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
-
"THEN #{quoted_right_column_name} + :a - :c " +
-
"ELSE #{quoted_right_column_name} END, " +
-
"#{quoted_parent_column_name} = CASE " +
-
"WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
-
"ELSE #{quoted_parent_column_name} END",
-
{:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
-
])
-
end
-
168
target.reload_nested_set if target
-
168
self.reload_nested_set
-
end
-
end
-
-
end
-
-
# Mixed into both classes and instances to provide easy access to the column names
-
1
module Columns
-
1
def left_column_name
-
48045
acts_as_nested_set_options[:left_column]
-
end
-
-
1
def right_column_name
-
35333
acts_as_nested_set_options[:right_column]
-
end
-
-
1
def parent_column_name
-
16549
acts_as_nested_set_options[:parent_column]
-
end
-
-
1
def scope_column_names
-
Array(acts_as_nested_set_options[:scope])
-
end
-
-
1
def quoted_left_column_name
-
29381
connection.quote_column_name(left_column_name)
-
end
-
-
1
def quoted_right_column_name
-
18673
connection.quote_column_name(right_column_name)
-
end
-
-
1
def quoted_parent_column_name
-
2164
connection.quote_column_name(parent_column_name)
-
end
-
-
1
def quoted_scope_column_names
-
scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
-
end
-
end
-
-
end
-
end
-
end
-
1
module CollectiveIdea #:nodoc:
-
1
module Acts #:nodoc:
-
1
module NestedSet #:nodoc:
-
# This module provides some helpers for the model classes using acts_as_nested_set.
-
# It is included by default in all views.
-
#
-
1
module Helper
-
# Returns options for select.
-
# You can exclude some items from the tree.
-
# You can pass a block receiving an item and returning the string displayed in the select.
-
#
-
# == Params
-
# * +class_or_item+ - Class name or top level times
-
# * +mover+ - The item that is being move, used to exlude impossible moves
-
# * +&block+ - a block that will be used to display: {Â |item| ... item.name }
-
#
-
# == Usage
-
#
-
# <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
-
# "#{'–' * i.level} #{i.name}"
-
# }) %>
-
#
-
1
def nested_set_options(class_or_item, mover = nil)
-
if class_or_item.is_a? Array
-
items = class_or_item.reject { |e| !e.root? }
-
else
-
class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
-
items = Array(class_or_item)
-
end
-
result = []
-
items.each do |root|
-
result += root.self_and_descendants.map do |i|
-
if mover.nil? || mover.new_record? || mover.move_possible?(i)
-
[yield(i), i.id]
-
end
-
end.compact
-
end
-
result
-
end
-
-
end
-
end
-
end
-
end
-
1
module ActionController
-
# === Action Pack pagination for Active Record collections
-
#
-
# The Pagination module aids in the process of paging large collections of
-
# Active Record objects. It offers macro-style automatic fetching of your
-
# model for multiple views, or explicit fetching for single actions. And if
-
# the magic isn't flexible enough for your needs, you can create your own
-
# paginators with a minimal amount of code.
-
#
-
# The Pagination module can handle as much or as little as you wish. In the
-
# controller, have it automatically query your model for pagination; or,
-
# if you prefer, create Paginator objects yourself.
-
#
-
# Pagination is included automatically for all controllers.
-
#
-
# For help rendering pagination links, see
-
# ActionView::Helpers::PaginationHelper.
-
#
-
# ==== Automatic pagination for every action in a controller
-
#
-
# class PersonController < ApplicationController
-
# model :person
-
#
-
# paginate :people, :order => 'last_name, first_name',
-
# :per_page => 20
-
#
-
# # ...
-
# end
-
#
-
# Each action in this controller now has access to a <tt>@people</tt>
-
# instance variable, which is an ordered collection of model objects for the
-
# current page (at most 20, sorted by last name and first name), and a
-
# <tt>@person_pages</tt> Paginator instance. The current page is determined
-
# by the <tt>params[:page]</tt> variable.
-
#
-
# ==== Pagination for a single action
-
#
-
# def list
-
# @person_pages, @people =
-
# paginate :people, :order => 'last_name, first_name'
-
# end
-
#
-
# Like the previous example, but explicitly creates <tt>@person_pages</tt>
-
# and <tt>@people</tt> for a single action, and uses the default of 10 items
-
# per page.
-
#
-
# ==== Custom/"classic" pagination
-
#
-
# def list
-
# @person_pages = Paginator.new self, Person.count, 10, params[:page]
-
# @people = Person.find :all, :order => 'last_name, first_name',
-
# :limit => @person_pages.items_per_page,
-
# :offset => @person_pages.current.offset
-
# end
-
#
-
# Explicitly creates the paginator from the previous example and uses
-
# Paginator#to_sql to retrieve <tt>@people</tt> from the model.
-
#
-
1
module Pagination
-
1
unless const_defined?(:OPTIONS)
-
# A hash holding options for controllers using macro-style pagination
-
1
OPTIONS = Hash.new
-
-
# The default options for pagination
-
1
DEFAULT_OPTIONS = {
-
:class_name => nil,
-
:singular_name => nil,
-
:per_page => 10,
-
:conditions => nil,
-
:order_by => nil,
-
:order => nil,
-
:join => nil,
-
:joins => nil,
-
:count => nil,
-
:include => nil,
-
:select => nil,
-
:group => nil,
-
:parameter => 'page'
-
}
-
else
-
DEFAULT_OPTIONS[:group] = nil
-
end
-
-
1
def self.included(base) #:nodoc:
-
1
super
-
1
base.extend(ClassMethods)
-
end
-
-
1
def self.validate_options!(collection_id, options, in_action) #:nodoc:
-
options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
-
-
valid_options = DEFAULT_OPTIONS.keys
-
valid_options << :actions unless in_action
-
-
unknown_option_keys = options.keys - valid_options
-
raise ActionController::ActionControllerError,
-
"Unknown options: #{unknown_option_keys.join(', ')}" unless
-
unknown_option_keys.empty?
-
-
options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s)
-
options[:class_name] ||= ActiveSupport::Inflector.camelize(options[:singular_name])
-
end
-
-
# Returns a paginator and a collection of Active Record model instances
-
# for the paginator's current page. This is designed to be used in a
-
# single action; to automatically paginate multiple actions, consider
-
# ClassMethods#paginate.
-
#
-
# +options+ are:
-
# <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
-
# <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
-
# camelizing the singular name
-
# <tt>:per_page</tt>:: the maximum number of items to include in a
-
# single page. Defaults to 10
-
# <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
-
# Model.count
-
# <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
-
# <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
-
# <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
-
# and Model.count
-
# <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
-
# and Model.count
-
# <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
-
# and Model.count
-
# <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
-
#
-
# <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
-
#
-
# <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
-
#
-
1
def paginate(collection_id, options={})
-
Pagination.validate_options!(collection_id, options, true)
-
paginator_and_collection_for(collection_id, options)
-
end
-
-
# These methods become class methods on any controller
-
1
module ClassMethods
-
# Creates a +before_filter+ which automatically paginates an Active
-
# Record model for all actions in a controller (or certain actions if
-
# specified with the <tt>:actions</tt> option).
-
#
-
# +options+ are the same as PaginationHelper#paginate, with the addition
-
# of:
-
# <tt>:actions</tt>:: an array of actions for which the pagination is
-
# active. Defaults to +nil+ (i.e., every action)
-
1
def paginate(collection_id, options={})
-
Pagination.validate_options!(collection_id, options, false)
-
module_eval do
-
before_filter :create_paginators_and_retrieve_collections
-
OPTIONS[self] ||= Hash.new
-
OPTIONS[self][collection_id] = options
-
end
-
end
-
end
-
-
1
def create_paginators_and_retrieve_collections #:nodoc:
-
Pagination::OPTIONS[self.class].each do |collection_id, options|
-
next unless options[:actions].include? action_name if
-
options[:actions]
-
-
paginator, collection =
-
paginator_and_collection_for(collection_id, options)
-
-
paginator_name = "@#{options[:singular_name]}_pages"
-
self.instance_variable_set(paginator_name, paginator)
-
-
collection_name = "@#{collection_id.to_s}"
-
self.instance_variable_set(collection_name, collection)
-
end
-
end
-
-
# Returns the total number of items in the collection to be paginated for
-
# the +model+ and given +conditions+. Override this method to implement a
-
# custom counter.
-
1
def count_collection_for_pagination(model, options)
-
model.count(:conditions => options[:conditions],
-
:joins => options[:join] || options[:joins],
-
:include => options[:include],
-
:select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
-
end
-
-
# Returns a collection of items for the given +model+ and +options[conditions]+,
-
# ordered by +options[order]+, for the current page in the given +paginator+.
-
# Override this method to implement a custom finder.
-
1
def find_collection_for_pagination(model, options, paginator)
-
model.find(:all, :conditions => options[:conditions],
-
:order => options[:order_by] || options[:order],
-
:joins => options[:join] || options[:joins], :include => options[:include],
-
:select => options[:select], :limit => options[:per_page],
-
:group => options[:group], :offset => paginator.current.offset)
-
end
-
-
1
protected :create_paginators_and_retrieve_collections,
-
:count_collection_for_pagination,
-
:find_collection_for_pagination
-
-
1
def paginator_and_collection_for(collection_id, options) #:nodoc:
-
klass = options[:class_name].constantize
-
page = params[options[:parameter]]
-
count = count_collection_for_pagination(klass, options)
-
paginator = Paginator.new(self, count, options[:per_page], page)
-
collection = find_collection_for_pagination(klass, options, paginator)
-
-
return paginator, collection
-
end
-
-
1
private :paginator_and_collection_for
-
-
# A class representing a paginator for an Active Record collection.
-
1
class Paginator
-
1
include Enumerable
-
-
# Creates a new Paginator on the given +controller+ for a set of items
-
# of size +item_count+ and having +items_per_page+ items per page.
-
# Raises ArgumentError if items_per_page is out of bounds (i.e., less
-
# than or equal to zero). The page CGI parameter for links defaults to
-
# "page" and can be overridden with +page_parameter+.
-
1
def initialize(controller, item_count, items_per_page, current_page=1)
-
raise ArgumentError, 'must have at least one item per page' if
-
20
items_per_page <= 0
-
-
20
@controller = controller
-
20
@item_count = item_count || 0
-
20
@items_per_page = items_per_page
-
20
@pages = {}
-
-
20
self.current_page = current_page
-
end
-
1
attr_reader :controller, :item_count, :items_per_page
-
-
# Sets the current page number of this paginator. If +page+ is a Page
-
# object, its +number+ attribute is used as the value; if the page does
-
# not belong to this Paginator, an ArgumentError is raised.
-
1
def current_page=(page)
-
20
if page.is_a? Page
-
raise ArgumentError, 'Page/Paginator mismatch' unless
-
page.paginator == self
-
end
-
20
page = page.to_i
-
20
@current_page_number = has_page_number?(page) ? page : 1
-
end
-
-
# Returns a Page object representing this paginator's current page.
-
1
def current_page
-
105
@current_page ||= self[@current_page_number]
-
end
-
1
alias current :current_page
-
-
# Returns a new Page representing the first page in this paginator.
-
1
def first_page
-
34
@first_page ||= self[1]
-
end
-
1
alias first :first_page
-
-
# Returns a new Page representing the last page in this paginator.
-
1
def last_page
-
34
@last_page ||= self[page_count]
-
end
-
1
alias last :last_page
-
-
# Returns the number of pages in this paginator.
-
1
def page_count
-
@page_count ||= @item_count.zero? ? 1 :
-
54
(q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
-
end
-
-
1
alias length :page_count
-
-
# Returns true if this paginator contains the page of index +number+.
-
1
def has_page_number?(number)
-
74
number >= 1 and number <= page_count
-
end
-
-
# Returns a new Page representing the page with the given index
-
# +number+.
-
1
def [](number)
-
71
@pages[number] ||= Page.new(self, number)
-
end
-
-
# Successively yields all the paginator's pages to the given block.
-
1
def each(&block)
-
page_count.times do |n|
-
yield self[n+1]
-
end
-
end
-
-
# A class representing a single page in a paginator.
-
1
class Page
-
1
include Comparable
-
-
# Creates a new Page for the given +paginator+ with the index
-
# +number+. If +number+ is not in the range of valid page numbers or
-
# is not a number at all, it defaults to 1.
-
1
def initialize(paginator, number)
-
20
@paginator = paginator
-
20
@number = number.to_i
-
20
@number = 1 unless @paginator.has_page_number? @number
-
end
-
1
attr_reader :paginator, :number
-
1
alias to_i :number
-
-
# Compares two Page objects and returns true when they represent the
-
# same page (i.e., their paginators are the same and they have the
-
# same page number).
-
1
def ==(page)
-
34
return false if page.nil?
-
@paginator == page.paginator and
-
34
@number == page.number
-
end
-
-
# Compares two Page objects and returns -1 if the left-hand page comes
-
# before the right-hand page, 0 if the pages are equal, and 1 if the
-
# left-hand page comes after the right-hand page. Raises ArgumentError
-
# if the pages do not belong to the same Paginator object.
-
1
def <=>(page)
-
raise ArgumentError unless @paginator == page.paginator
-
@number <=> page.number
-
end
-
-
# Returns the item offset for the first item in this page.
-
1
def offset
-
37
@paginator.items_per_page * (@number - 1)
-
end
-
-
# Returns the number of the first item displayed.
-
1
def first_item
-
17
offset + 1
-
end
-
-
# Returns the number of the last item displayed.
-
1
def last_item
-
17
[@paginator.items_per_page * @number, @paginator.item_count].min
-
end
-
-
# Returns true if this page is the first page in the paginator.
-
1
def first?
-
17
self == @paginator.first
-
end
-
-
# Returns true if this page is the last page in the paginator.
-
1
def last?
-
17
self == @paginator.last
-
end
-
-
# Returns a new Page object representing the page just before this
-
# page, or nil if this is the first page.
-
1
def previous
-
17
if first? then nil else @paginator[@number - 1] end
-
end
-
-
# Returns a new Page object representing the page just after this
-
# page, or nil if this is the last page.
-
1
def next
-
17
if last? then nil else @paginator[@number + 1] end
-
end
-
-
# Returns a new Window object for this page with the specified
-
# +padding+.
-
1
def window(padding=2)
-
17
Window.new(self, padding)
-
end
-
-
# Returns the limit/offset array for this page.
-
1
def to_sql
-
[@paginator.items_per_page, offset]
-
end
-
-
1
def to_param #:nodoc:
-
@number.to_s
-
end
-
end
-
-
# A class for representing ranges around a given page.
-
1
class Window
-
# Creates a new Window object for the given +page+ with the specified
-
# +padding+.
-
1
def initialize(page, padding=2)
-
17
@paginator = page.paginator
-
17
@page = page
-
17
self.padding = padding
-
end
-
1
attr_reader :paginator, :page
-
-
# Sets the window's padding (the number of pages on either side of the
-
# window page).
-
1
def padding=(padding)
-
17
@padding = padding < 0 ? 0 : padding
-
# Find the beginning and end pages of the window
-
17
@first = @paginator.has_page_number?(@page.number - @padding) ?
-
@paginator[@page.number - @padding] : @paginator.first
-
17
@last = @paginator.has_page_number?(@page.number + @padding) ?
-
@paginator[@page.number + @padding] : @paginator.last
-
end
-
1
attr_reader :padding, :first, :last
-
-
# Returns an array of Page objects in the current window.
-
1
def pages
-
34
(@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
-
end
-
1
alias to_a :pages
-
end
-
end
-
-
end
-
end
-
1
module ActionView
-
1
module Helpers
-
# Provides methods for linking to ActionController::Pagination objects using a simple generator API. You can optionally
-
# also build your links manually using ActionView::Helpers::AssetHelper#link_to like so:
-
#
-
# <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %>
-
# <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %>
-
1
module PaginationHelper
-
1
unless const_defined?(:DEFAULT_OPTIONS)
-
1
DEFAULT_OPTIONS = {
-
:name => :page,
-
:window_size => 2,
-
:always_show_anchors => true,
-
:link_to_current_page => false,
-
:params => {}
-
}
-
end
-
-
# Creates a basic HTML link bar for the given +paginator+. Links will be created
-
# for the next and/or previous page and for a number of other pages around the current
-
# pages position. The +html_options+ hash is passed to +link_to+ when the links are created.
-
#
-
# ==== Options
-
# <tt>:name</tt>:: the routing name for this paginator
-
# (defaults to +page+)
-
# <tt>:prefix</tt>:: prefix for pagination links
-
# (i.e. Older Pages: 1 2 3 4)
-
# <tt>:suffix</tt>:: suffix for pagination links
-
# (i.e. 1 2 3 4 <- Older Pages)
-
# <tt>:window_size</tt>:: the number of pages to show around
-
# the current page (defaults to <tt>2</tt>)
-
# <tt>:always_show_anchors</tt>:: whether or not the first and last
-
# pages should always be shown
-
# (defaults to +true+)
-
# <tt>:link_to_current_page</tt>:: whether or not the current page
-
# should be linked to (defaults to
-
# +false+)
-
# <tt>:params</tt>:: any additional routing parameters
-
# for page URLs
-
#
-
# ==== Examples
-
# # We'll assume we have a paginator setup in @person_pages...
-
#
-
# pagination_links(@person_pages)
-
# # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
-
#
-
# pagination_links(@person_pages, :link_to_current_page => true)
-
# # => <a href="/?page=1/">1</a> <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
-
#
-
# pagination_links(@person_pages, :always_show_anchors => false)
-
# # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a>
-
#
-
# pagination_links(@person_pages, :window_size => 1)
-
# # => 1 <a href="/?page=2/">2</a> ... <a href="/?page=10/">10</a>
-
#
-
# pagination_links(@person_pages, :params => { :viewer => "flash" })
-
# # => 1 <a href="/?page=2&viewer=flash/">2</a> <a href="/?page=3&viewer=flash/">3</a> ...
-
# # <a href="/?page=10&viewer=flash/">10</a>
-
1
def pagination_links(paginator, options={}, html_options={})
-
name = options[:name] || DEFAULT_OPTIONS[:name]
-
params = (options[:params] || DEFAULT_OPTIONS[:params]).clone
-
-
prefix = options[:prefix] || ''
-
suffix = options[:suffix] || ''
-
-
pagination_links_each(paginator, options, prefix, suffix) do |n|
-
params[name] = n
-
link_to(n.to_s, params, html_options)
-
end
-
end
-
-
# Iterate through the pages of a given +paginator+, invoking a
-
# block for each page number that needs to be rendered as a link.
-
#
-
# ==== Options
-
# <tt>:window_size</tt>:: the number of pages to show around
-
# the current page (defaults to +2+)
-
# <tt>:always_show_anchors</tt>:: whether or not the first and last
-
# pages should always be shown
-
# (defaults to +true+)
-
# <tt>:link_to_current_page</tt>:: whether or not the current page
-
# should be linked to (defaults to
-
# +false+)
-
#
-
# ==== Example
-
# # Turn paginated links into an Ajax call
-
# pagination_links_each(paginator, page_options) do |link|
-
# options = { :url => {:action => 'list'}, :update => 'results' }
-
# html_options = { :href => url_for(:action => 'list') }
-
#
-
# link_to_remote(link.to_s, options, html_options)
-
# end
-
1
def pagination_links_each(paginator, options, prefix = nil, suffix = nil)
-
17
options = DEFAULT_OPTIONS.merge(options)
-
17
link_to_current_page = options[:link_to_current_page]
-
17
always_show_anchors = options[:always_show_anchors]
-
-
17
current_page = paginator.current_page
-
17
window_pages = current_page.window(options[:window_size]).pages
-
17
return if window_pages.length <= 1 unless link_to_current_page
-
-
first, last = paginator.first, paginator.last
-
-
html = ''
-
-
html << prefix if prefix
-
-
if always_show_anchors and not (wp_first = window_pages[0]).first?
-
html << yield(first.number)
-
html << ' ... ' if wp_first.number - first.number > 1
-
html << ' '
-
end
-
-
window_pages.each do |page|
-
if current_page == page && !link_to_current_page
-
html << page.number.to_s
-
else
-
html << yield(page.number)
-
end
-
html << ' '
-
end
-
-
if always_show_anchors and not (wp_last = window_pages[-1]).last?
-
html << ' ... ' if last.number - wp_last.number > 1
-
html << yield(last.number)
-
end
-
-
html << suffix if suffix
-
-
html
-
end
-
-
end # PaginationHelper
-
end # Helpers
-
end # ActionView
-
1
require 'digest/md5'
-
1
require 'cgi'
-
-
1
module GravatarHelper
-
-
# These are the options that control the default behavior of the public
-
# methods. They can be overridden during the actual call to the helper,
-
# or you can set them in your environment.rb as such:
-
#
-
# # Allow racier gravatars
-
# GravatarHelper::DEFAULT_OPTIONS[:rating] = 'R'
-
#
-
1
DEFAULT_OPTIONS = {
-
# The URL of a default image to display if the given email address does
-
# not have a gravatar.
-
:default => nil,
-
-
# The default size in pixels for the gravatar image (they're square).
-
:size => 50,
-
-
# The maximum allowed MPAA rating for gravatars. This allows you to
-
# exclude gravatars that may be out of character for your site.
-
:rating => 'PG',
-
-
# The alt text to use in the img tag for the gravatar. Since it's a
-
# decorational picture, the alt text should be empty according to the
-
# XHTML specs.
-
:alt => '',
-
-
# The title text to use for the img tag for the gravatar.
-
:title => '',
-
-
# The class to assign to the img tag for the gravatar.
-
:class => 'gravatar',
-
-
# Whether or not to display the gravatars using HTTPS instead of HTTP
-
:ssl => false,
-
}
-
-
# The methods that will be made available to your views.
-
1
module PublicMethods
-
-
# Return the HTML img tag for the given user's gravatar. Presumes that
-
# the given user object will respond_to "email", and return the user's
-
# email address.
-
1
def gravatar_for(user, options={})
-
gravatar(user.email, options)
-
end
-
-
# Return the HTML img tag for the given email address's gravatar.
-
1
def gravatar(email, options={})
-
src = h(gravatar_url(email, options))
-
options = DEFAULT_OPTIONS.merge(options)
-
[:class, :alt, :title].each { |opt| options[opt] = h(options[opt]) }
-
image_tag src, options
-
end
-
-
# Returns the base Gravatar URL for the given email hash. If ssl evaluates to true,
-
# a secure URL will be used instead. This is required when the gravatar is to be
-
# displayed on a HTTPS site.
-
1
def gravatar_api_url(hash, ssl=false)
-
2
if ssl
-
"https://secure.gravatar.com/avatar/#{hash}"
-
else
-
2
"http://www.gravatar.com/avatar/#{hash}"
-
end
-
end
-
-
# Return the gravatar URL for the given email address.
-
1
def gravatar_url(email, options={})
-
2
email_hash = Digest::MD5.hexdigest(email)
-
2
options = DEFAULT_OPTIONS.merge(options)
-
2
options[:default] = CGI::escape(options[:default]) unless options[:default].nil?
-
2
gravatar_api_url(email_hash, options.delete(:ssl)).tap do |url|
-
2
opts = []
-
2
[:rating, :size, :default].each do |opt|
-
6
unless options[opt].nil?
-
4
value = h(options[opt])
-
4
opts << [opt, value].join('=')
-
end
-
end
-
2
url << "?#{opts.join('&')}" unless opts.empty?
-
end
-
end
-
-
end
-
-
end
-
1
require 'uri'
-
1
require 'openid'
-
1
require 'rack/openid'
-
-
1
module OpenIdAuthentication
-
1
def self.new(app)
-
1
store = OpenIdAuthentication.store
-
1
if store.nil?
-
1
Rails.logger.warn "OpenIdAuthentication.store is nil. Using in-memory store."
-
end
-
-
1
::Rack::OpenID.new(app, OpenIdAuthentication.store)
-
end
-
-
1
def self.store
-
2
@@store
-
end
-
-
1
def self.store=(*store_option)
-
1
store, *parameters = *([ store_option ].flatten)
-
-
1
@@store = case store
-
when :memory
-
require 'openid/store/memory'
-
OpenID::Store::Memory.new
-
when :file
-
require 'openid/store/filesystem'
-
OpenID::Store::Filesystem.new(Rails.root.join('tmp/openids'))
-
when :memcache
-
require 'memcache'
-
require 'openid/store/memcache'
-
OpenID::Store::Memcache.new(MemCache.new(parameters))
-
else
-
1
store
-
end
-
end
-
-
1
self.store = nil
-
-
1
class InvalidOpenId < StandardError
-
end
-
-
1
class Result
-
1
ERROR_MESSAGES = {
-
:missing => "Sorry, the OpenID server couldn't be found",
-
:invalid => "Sorry, but this does not appear to be a valid OpenID",
-
:canceled => "OpenID verification was canceled",
-
:failed => "OpenID verification failed",
-
:setup_needed => "OpenID verification needs setup"
-
}
-
-
1
def self.[](code)
-
new(code)
-
end
-
-
1
def initialize(code)
-
@code = code
-
end
-
-
1
def status
-
@code
-
end
-
-
6
ERROR_MESSAGES.keys.each { |state| define_method("#{state}?") { @code == state } }
-
-
1
def successful?
-
@code == :successful
-
end
-
-
1
def unsuccessful?
-
ERROR_MESSAGES.keys.include?(@code)
-
end
-
-
1
def message
-
ERROR_MESSAGES[@code]
-
end
-
end
-
-
# normalizes an OpenID according to http://openid.net/specs/openid-authentication-2_0.html#normalization
-
1
def self.normalize_identifier(identifier)
-
# clean up whitespace
-
identifier = identifier.to_s.strip
-
-
# if an XRI has a prefix, strip it.
-
identifier.gsub!(/xri:\/\//i, '')
-
-
# dodge XRIs -- TODO: validate, don't just skip.
-
unless ['=', '@', '+', '$', '!', '('].include?(identifier.at(0))
-
# does it begin with http? if not, add it.
-
identifier = "http://#{identifier}" unless identifier =~ /^http/i
-
-
# strip any fragments
-
identifier.gsub!(/\#(.*)$/, '')
-
-
begin
-
uri = URI.parse(identifier)
-
uri.scheme = uri.scheme.downcase if uri.scheme # URI should do this
-
identifier = uri.normalize.to_s
-
rescue URI::InvalidURIError
-
raise InvalidOpenId.new("#{identifier} is not an OpenID identifier")
-
end
-
end
-
-
return identifier
-
end
-
-
1
protected
-
# The parameter name of "openid_identifier" is used rather than
-
# the Rails convention "open_id_identifier" because that's what
-
# the specification dictates in order to get browser auto-complete
-
# working across sites
-
1
def using_open_id?(identifier = nil) #:doc:
-
identifier ||= open_id_identifier
-
!identifier.blank? || request.env[Rack::OpenID::RESPONSE]
-
end
-
-
1
def authenticate_with_open_id(identifier = nil, options = {}, &block) #:doc:
-
identifier ||= open_id_identifier
-
-
if request.env[Rack::OpenID::RESPONSE]
-
complete_open_id_authentication(&block)
-
else
-
begin_open_id_authentication(identifier, options, &block)
-
end
-
end
-
-
1
private
-
1
def open_id_identifier
-
params[:openid_identifier] || params[:openid_url]
-
end
-
-
1
def begin_open_id_authentication(identifier, options = {})
-
options[:identifier] = identifier
-
value = Rack::OpenID.build_header(options)
-
response.headers[Rack::OpenID::AUTHENTICATE_HEADER] = value
-
head :unauthorized
-
end
-
-
1
def complete_open_id_authentication
-
response = request.env[Rack::OpenID::RESPONSE]
-
identifier = response.display_identifier
-
-
case response.status
-
when OpenID::Consumer::SUCCESS
-
yield Result[:successful], identifier,
-
OpenID::SReg::Response.from_success_response(response)
-
when :missing
-
yield Result[:missing], identifier, nil
-
when :invalid
-
yield Result[:invalid], identifier, nil
-
when OpenID::Consumer::CANCEL
-
yield Result[:canceled], identifier, nil
-
when OpenID::Consumer::FAILURE
-
yield Result[:failed], identifier, nil
-
when OpenID::Consumer::SETUP_NEEDED
-
yield Result[:setup_needed], response.setup_url, nil
-
end
-
end
-
end
-
1
module Core::RFPDF
-
1
COLOR_PALETTE = {
-
:black => [0x00, 0x00, 0x00],
-
:white => [0xff, 0xff, 0xff],
-
}.freeze
-
-
# Draw a circle at (<tt>mid_x, mid_y</tt>) with <tt>radius</tt>.
-
#
-
# Options are:
-
# * <tt>:border</tt> - Draw a border, 0 = no, 1 = yes? Default value is <tt>1</tt>.
-
# * <tt>:border_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:border_width</tt> - Default value is <tt>0.5</tt>.
-
# * <tt>:fill</tt> - Fill the box, 0 = no, 1 = yes? Default value is <tt>1</tt>.
-
# * <tt>:fill_color</tt> - Default value is nothing or <tt>COLOR_PALETTE[:white]</tt>.
-
# * <tt>:fill_colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_circle(x, y, radius, :border_color => ReportHelper::COLOR_PALETTE[:dark_blue], :border_width => 1)
-
#
-
1
def draw_circle(mid_x, mid_y, radius, options = {})
-
options[:border] ||= 1
-
options[:border_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:border_width] ||= 0.5
-
options[:fill] ||= 1
-
options[:fill_color] ||= Core::RFPDF::COLOR_PALETTE[:white]
-
options[:fill_colorspace] ||= :rgb
-
SetLineWidth(options[:border_width])
-
set_draw_color_a(options[:border_color])
-
set_fill_color_a(options[:fill_color], options[:colorspace])
-
fd = ""
-
fd = "D" if options[:border] == 1
-
fd += "F" if options[:fill] == 1
-
Circle(mid_x, mid_y, radius, fd)
-
end
-
-
# Draw a line from (<tt>x1, y1</tt>) to (<tt>x2, y2</tt>).
-
#
-
# Options are:
-
# * <tt>:line_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:line_width</tt> - Default value is <tt>0.5</tt>.
-
#
-
# Example:
-
#
-
# draw_line(x1, y1, x1, y1+h, :line_color => ReportHelper::COLOR_PALETTE[:dark_blue], :line_width => 1)
-
#
-
1
def draw_line(x1, y1, x2, y2, options = {})
-
options[:line_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:line_width] ||= 0.5
-
set_draw_color_a(options[:line_color])
-
SetLineWidth(options[:line_width])
-
Line(x1, y1, x2, y2)
-
end
-
-
# Draw a string of <tt>text</tt> at (<tt>x, y</tt>).
-
#
-
# Options are:
-
# * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:font_size</tt> - Default value is <tt>10</tt>.
-
# * <tt>:font_style</tt> - Default value is nothing or <tt>''</tt>.
-
# * <tt>:colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_text(x, y, header_left, :font_size => 10)
-
#
-
1
def draw_text(x, y, text, options = {})
-
options[:font_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:font] ||= default_font
-
options[:font_size] ||= 10
-
options[:font_style] ||= ''
-
set_text_color_a(options[:font_color], options[:colorspace])
-
SetFont(options[:font], options[:font_style], options[:font_size])
-
SetXY(x, y)
-
Write(options[:font_size] + 4, text)
-
end
-
-
# Draw a block of <tt>text</tt> at (<tt>x, y</tt>) bounded by <tt>left_margin</tt> and <tt>right_margin_from_right_edge</tt>. Both
-
# margins are measured from their corresponding edge.
-
#
-
# Options are:
-
# * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:font_size</tt> - Default value is <tt>10</tt>.
-
# * <tt>:font_style</tt> - Default value is nothing or <tt>''</tt>.
-
# * <tt>:colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_text_block(left_margin, 85, "question", left_margin, 280,
-
# :font_color => ReportHelper::COLOR_PALETTE[:dark_blue],
-
# :font_size => 12,
-
# :font_style => 'I')
-
#
-
1
def draw_text_block(x, y, text, left_margin, right_margin_from_right_edge, options = {})
-
options[:font] ||= default_font
-
options[:font_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:font_size] ||= 10
-
options[:font_style] ||= ''
-
set_text_color_a(options[:font_color], options[:colorspace])
-
SetFont(options[:font], options[:font_style], options[:font_size])
-
SetXY(x, y)
-
SetLeftMargin(left_margin)
-
SetRightMargin(right_margin_from_right_edge)
-
Write(options[:font_size] + 4, text)
-
SetMargins(0,0,0)
-
end
-
-
# Draw a box at (<tt>x, y</tt>), <tt>w</tt> wide and <tt>h</tt> high.
-
#
-
# Options are:
-
# * <tt>:border</tt> - Draw a border, 0 = no, 1 = yes? Default value is <tt>1</tt>.
-
# * <tt>:border_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:border_width</tt> - Default value is <tt>0.5</tt>.
-
# * <tt>:fill</tt> - Fill the box, 0 = no, 1 = yes? Default value is <tt>1</tt>.
-
# * <tt>:fill_color</tt> - Default value is nothing or <tt>COLOR_PALETTE[:white]</tt>.
-
# * <tt>:fill_colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_box(x, y - 1, 38, 22)
-
#
-
1
def draw_box(x, y, w, h, options = {})
-
options[:border] ||= 1
-
options[:border_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:border_width] ||= 0.5
-
options[:fill] ||= 1
-
options[:fill_color] ||= Core::RFPDF::COLOR_PALETTE[:white]
-
options[:fill_colorspace] ||= :rgb
-
SetLineWidth(options[:border_width])
-
set_draw_color_a(options[:border_color])
-
set_fill_color_a(options[:fill_color], options[:fill_colorspace])
-
fd = ""
-
fd = "D" if options[:border] == 1
-
fd += "F" if options[:fill] == 1
-
Rect(x, y, w, h, fd)
-
end
-
-
# Draw a string of <tt>text</tt> at (<tt>x, y</tt>) in a box <tt>w</tt> wide and <tt>h</tt> high.
-
#
-
# Options are:
-
# * <tt>:align</tt> - Vertical alignment 'C' = center, 'L' = left, 'R' = right. Default value is <tt>'C'</tt>.
-
# * <tt>:border</tt> - Draw a border, 0 = no, 1 = yes? Default value is <tt>0</tt>.
-
# * <tt>:border_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:border_width</tt> - Default value is <tt>0.5</tt>.
-
# * <tt>:fill</tt> - Fill the box, 0 = no, 1 = yes? Default value is <tt>1</tt>.
-
# * <tt>:fill_color</tt> - Default value is nothing or <tt>COLOR_PALETTE[:white]</tt>.
-
# * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:font_size</tt> - Default value is nothing or <tt>8</tt>.
-
# * <tt>:font_style</tt> - 'B' = bold, 'I' = italic, 'U' = underline. Default value is nothing <tt>''</tt>.
-
# * <tt>:padding</tt> - Default value is nothing or <tt>2</tt>.
-
# * <tt>:x_padding</tt> - Default value is nothing.
-
# * <tt>:valign</tt> - 'M' = middle, 'T' = top, 'B' = bottom. Default value is nothing or <tt>'M'</tt>.
-
# * <tt>:colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_text_box(x, y - 1, 38, 22,
-
# "your_score_title",
-
# :fill => 0,
-
# :font_color => ReportHelper::COLOR_PALETTE[:blue],
-
# :font_line_spacing => 0,
-
# :font_style => "B",
-
# :valign => "M")
-
#
-
1
def draw_text_box(x, y, w, h, text, options = {})
-
options[:align] ||= 'C'
-
options[:border] ||= 0
-
options[:border_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:border_width] ||= 0.5
-
options[:fill] ||= 1
-
options[:fill_color] ||= Core::RFPDF::COLOR_PALETTE[:white]
-
options[:font] ||= default_font
-
options[:font_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:font_size] ||= 8
-
options[:font_line_spacing] ||= options[:font_size] * 0.3
-
options[:font_style] ||= ''
-
options[:padding] ||= 2
-
options[:x_padding] ||= 0
-
options[:valign] ||= "M"
-
if options[:fill] == 1 or options[:border] == 1
-
draw_box(x, y, w, h, options)
-
end
-
SetMargins(0,0,0)
-
set_text_color_a(options[:font_color], options[:colorspace])
-
font_size = options[:font_size]
-
SetFont(options[:font], options[:font_style], font_size)
-
font_size += options[:font_line_spacing]
-
case options[:valign]
-
when "B", "bottom"
-
y -= options[:padding]
-
when "T", "top"
-
y += options[:padding]
-
end
-
case options[:align]
-
when "L", "left"
-
x += options[:x_padding]
-
w -= options[:x_padding]
-
w -= options[:x_padding]
-
when "R", "right"
-
x += options[:x_padding]
-
w -= options[:x_padding]
-
w -= options[:x_padding]
-
end
-
SetXY(x, y)
-
if GetStringWidth(text) < w or not text["\n"].nil? and (options[:valign] == "T" || options[:valign] == "top")
-
text = text + "\n"
-
end
-
if GetStringWidth(text) > w or not text["\n"].nil? or (options[:valign] == "B" || options[:valign] == "bottom")
-
font_size += options[:font_size] * 0.1
-
# TODO 2006-07-21 Level=1 - this is assuming a 2 line text
-
SetXY(x, y + ((h - (font_size * 2)) / 2)) if (options[:valign] == "M" || options[:valign] == "middle")
-
MultiCell(w, font_size, text, 0, options[:align])
-
else
-
Cell(w, h, text, 0, 0, options[:align])
-
end
-
end
-
-
# Draw a string of <tt>text</tt> at (<tt>x, y</tt>) as a title.
-
#
-
# Options are:
-
# * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:font_size</tt> - Default value is <tt>18</tt>.
-
# * <tt>:font_style</tt> - Default value is nothing or <tt>''</tt>.
-
# * <tt>:colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_title(left_margin, 60,
-
# "title:",
-
# :font_color => ReportHelper::COLOR_PALETTE[:dark_blue])
-
#
-
1
def draw_title(x, y, title, options = {})
-
options[:font_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:font] ||= default_font
-
options[:font_size] ||= 18
-
options[:font_style] ||= ''
-
set_text_color_a(options[:font_color], options[:colorspace])
-
SetFont(options[:font], options[:font_style], options[:font_size])
-
SetXY(x, y)
-
Write(options[:font_size] + 2, title)
-
end
-
-
# Set the draw color. Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
#
-
# Example:
-
#
-
# set_draw_color_a(ReportHelper::COLOR_PALETTE[:dark_blue])
-
#
-
1
def set_draw_color_a(color = Core::RFPDF::COLOR_PALETTE[:black])
-
SetDrawColor(color[0], color[1], color[2])
-
end
-
-
# Set the fill color. Default value is <tt>COLOR_PALETTE[:white]</tt>.
-
#
-
# Example:
-
#
-
# set_fill_color_a(ReportHelper::COLOR_PALETTE[:dark_blue])
-
#
-
1
def set_fill_color_a(color = Core::RFPDF::COLOR_PALETTE[:white], colorspace = :rgb)
-
if colorspace == :cmyk
-
SetCmykFillColor(color[0], color[1], color[2], color[3])
-
else
-
SetFillColor(color[0], color[1], color[2])
-
end
-
end
-
-
# Set the text color. Default value is <tt>COLOR_PALETTE[:white]</tt>.
-
#
-
# Example:
-
#
-
# set_text_color_a(ReportHelper::COLOR_PALETTE[:dark_blue])
-
#
-
1
def set_text_color_a(color = Core::RFPDF::COLOR_PALETTE[:black], colorspace = :rgb)
-
if colorspace == :cmyk
-
SetCmykTextColor(color[0], color[1], color[2], color[3])
-
else
-
SetTextColor(color[0], color[1], color[2])
-
end
-
end
-
-
# Write a string containing html characters. Default value is <tt>COLOR_PALETTE[:white]</tt>.
-
#
-
# Options are:
-
# * <tt>:height</tt> - Line height. Default value is <tt>20</tt>.
-
#
-
# Example:
-
#
-
# write_html_with_options(html, :height => 12)
-
#
-
#FIXME 2007-08-07 (EJM) Level=0 - This needs to call the TCPDF version.
-
1
def write_html_with_options(html, options = {})
-
options[:fill] ||= 0
-
options[:height] ||= 20
-
options[:new_line_after] ||= false
-
write_html(html, options[:new_line_after], options[:fill], options[:height])
-
return
-
end
-
end
-
# The MIT License
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
#
-
# This implements native php methods used by tcpdf, which have had to be
-
# reimplemented within Ruby.
-
-
1
module RFPDF
-
-
# http://uk2.php.net/getimagesize
-
1
def getimagesize(filename)
-
image = Magick::ImageList.new(filename)
-
-
out = Hash.new
-
out[0] = image.columns
-
out[1] = image.rows
-
-
# These are actually meant to return integer values But I couldn't seem to find anything saying what those values are.
-
# So for now they return strings. The only place that uses this at the moment is the parsejpeg method, so I've changed that too.
-
case image.mime_type
-
when "image/gif"
-
out[2] = "GIF"
-
when "image/jpeg"
-
out[2] = "JPEG"
-
when "image/png"
-
out[2] = "PNG"
-
when " image/vnd.wap.wbmp"
-
out[2] = "WBMP"
-
when "image/x-xpixmap"
-
out[2] = "XPM"
-
end
-
out[3] = "height=\"#{image.rows}\" width=\"#{image.columns}\""
-
out['mime'] = image.mime_type
-
-
# This needs work to cover more situations
-
# I can't see how to just list the number of channels with ImageMagick / rmagick
-
if image.colorspace.to_s == "CMYKColorspace"
-
out['channels'] = 4
-
elsif image.colorspace.to_s == "RGBColorspace"
-
out['channels'] = 3
-
end
-
-
out['bits'] = image.channel_depth
-
-
out
-
end
-
-
end
-
# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
-
# 1.12 contributed by Ed Moss.
-
#
-
# The MIT License
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
#
-
# This is direct port of chinese.php
-
#
-
# Chinese PDF support.
-
#
-
# Usage is as follows:
-
#
-
# require 'fpdf'
-
# require 'chinese'
-
# pdf = FPDF.new
-
# pdf.extend(PDF_Chinese)
-
#
-
# This allows it to be combined with other extensions, such as the bookmark
-
# module.
-
-
1
module PDF_Chinese
-
-
1
Big5_widths={' '=>250,'!'=>250,'"'=>408,'#'=>668,'$'=>490,'%'=>875,'&'=>698,'\''=>250,
-
'('=>240,')'=>240,'*'=>417,'+'=>667,','=>250,'-'=>313,'.'=>250,'/'=>520,'0'=>500,'1'=>500,
-
'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>250,';'=>250,
-
'<'=>667,'='=>667,'>'=>667,'?'=>396,'@'=>921,'A'=>677,'B'=>615,'C'=>719,'D'=>760,'E'=>625,
-
'F'=>552,'G'=>771,'H'=>802,'I'=>354,'J'=>354,'K'=>781,'L'=>604,'M'=>927,'N'=>750,'O'=>823,
-
'P'=>563,'Q'=>823,'R'=>729,'S'=>542,'T'=>698,'U'=>771,'V'=>729,'W'=>948,'X'=>771,'Y'=>677,
-
'Z'=>635,'['=>344,'\\'=>520,']'=>344,'^'=>469,'_'=>500,'`'=>250,'a'=>469,'b'=>521,'c'=>427,
-
'd'=>521,'e'=>438,'f'=>271,'g'=>469,'h'=>531,'i'=>250,'j'=>250,'k'=>458,'l'=>240,'m'=>802,
-
'n'=>531,'o'=>500,'p'=>521,'q'=>521,'r'=>365,'s'=>333,'t'=>292,'u'=>521,'v'=>458,'w'=>677,
-
'x'=>479,'y'=>458,'z'=>427,'{'=>480,'|'=>496,'}'=>480,'~'=>667}
-
-
1
GB_widths={' '=>207,'!'=>270,'"'=>342,'#'=>467,'$'=>462,'%'=>797,'&'=>710,'\''=>239,
-
'('=>374,')'=>374,'*'=>423,'+'=>605,','=>238,'-'=>375,'.'=>238,'/'=>334,'0'=>462,'1'=>462,
-
'2'=>462,'3'=>462,'4'=>462,'5'=>462,'6'=>462,'7'=>462,'8'=>462,'9'=>462,':'=>238,';'=>238,
-
'<'=>605,'='=>605,'>'=>605,'?'=>344,'@'=>748,'A'=>684,'B'=>560,'C'=>695,'D'=>739,'E'=>563,
-
'F'=>511,'G'=>729,'H'=>793,'I'=>318,'J'=>312,'K'=>666,'L'=>526,'M'=>896,'N'=>758,'O'=>772,
-
'P'=>544,'Q'=>772,'R'=>628,'S'=>465,'T'=>607,'U'=>753,'V'=>711,'W'=>972,'X'=>647,'Y'=>620,
-
'Z'=>607,'['=>374,'\\'=>333,']'=>374,'^'=>606,'_'=>500,'`'=>239,'a'=>417,'b'=>503,'c'=>427,
-
'd'=>529,'e'=>415,'f'=>264,'g'=>444,'h'=>518,'i'=>241,'j'=>230,'k'=>495,'l'=>228,'m'=>793,
-
'n'=>527,'o'=>524,'p'=>524,'q'=>504,'r'=>338,'s'=>336,'t'=>277,'u'=>517,'v'=>450,'w'=>652,
-
'x'=>466,'y'=>452,'z'=>407,'{'=>370,'|'=>258,'}'=>370,'~'=>605}
-
-
1
def AddCIDFont(family,style,name,cw,cMap,registry)
-
#ActionController::Base::logger.debug registry.to_a.join(":").to_s
-
fontkey=family.downcase+style.upcase
-
unless @fonts[fontkey].nil?
-
Error("Font already added: family style")
-
end
-
i=@fonts.length+1
-
name=name.gsub(' ','')
-
@fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'ut'=>40,'cw'=>cw, 'CMap'=>cMap,'registry'=>registry}
-
end
-
-
1
def AddCIDFonts(family,name,cw,cMap,registry)
-
AddCIDFont(family,'',name,cw,cMap,registry)
-
AddCIDFont(family,'B',name+',Bold',cw,cMap,registry)
-
AddCIDFont(family,'I',name+',Italic',cw,cMap,registry)
-
AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry)
-
end
-
-
1
def AddBig5Font(family='Big5',name='MSungStd-Light-Acro')
-
#Add Big5 font with proportional Latin
-
cw=Big5_widths
-
cMap='ETenms-B5-H'
-
registry={'ordering'=>'CNS1','supplement'=>0}
-
#ActionController::Base::logger.debug registry.to_a.join(":").to_s
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def AddBig5hwFont(family='Big5-hw',name='MSungStd-Light-Acro')
-
#Add Big5 font with half-witdh Latin
-
cw = {}
-
32.upto(126) do |i|
-
cw[i.chr]=500
-
end
-
cMap='ETen-B5-H'
-
registry={'ordering'=>'CNS1','supplement'=>0}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def AddGBFont(family='GB',name='STSongStd-Light-Acro')
-
#Add GB font with proportional Latin
-
cw=GB_widths
-
cMap='GBKp-EUC-H'
-
registry={'ordering'=>'GB1','supplement'=>2}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def AddGBhwFont(family='GB-hw',name='STSongStd-Light-Acro')
-
#Add GB font with half-width Latin
-
32.upto(126) do |i|
-
cw[i.chr]=500
-
end
-
cMap='GBK-EUC-H'
-
registry={'ordering'=>'GB1','supplement'=>2}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def GetStringWidth(s)
-
if(@current_font['type']=='Type0')
-
return GetMBStringWidth(s)
-
else
-
return super(s)
-
end
-
end
-
-
1
def GetMBStringWidth(s)
-
#Multi-byte version of GetStringWidth()
-
l=0
-
cw=@current_font['cw']
-
nb=s.length
-
i=0
-
while(i<nb)
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
if(c<128)
-
l+=cw[c.chr] if cw[c.chr]
-
i+=1
-
else
-
l+=1000
-
i+=2
-
end
-
end
-
return l*@font_size/1000
-
end
-
-
1
def MultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
if(@current_font['type']=='Type0')
-
MBMultiCell(w,h,txt,border,align,fill,ln)
-
else
-
super(w,h,txt,border,align,fill,ln)
-
end
-
end
-
-
1
def MBMultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
-
# save current position
-
prevx = @x;
-
prevy = @y;
-
-
#Multi-byte version of MultiCell()
-
cw=@current_font['cw']
-
if(w==0)
-
w=@w-@r_margin-@x
-
end
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
if(nb>0 and s[nb-1]=="\n")
-
nb-=1
-
end
-
b=0
-
if(border)
-
if(border==1)
-
border='LTRB'
-
b='LRT'
-
b2='LR'
-
else
-
b2=''
-
b2='L' unless border.to_s.index('L').nil?
-
b2=b2+'R' unless border.to_s.index('R').nil?
-
b=(border.to_s.index('T')) ? (b2+'T') : b2
-
end
-
end
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
#Check if ASCII or MB
-
ascii=(c<128)
-
if(c.chr=="\n")
-
#Explicit line break
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
next
-
end
-
if(!ascii)
-
sep=i
-
ls=l
-
elsif(c.chr==' ')
-
sep=i
-
ls=l
-
end
-
l+=(ascii ? cw[c.chr] : 1000) || 0
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(i==j)
-
i+=ascii ? 1 : 2
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
else
-
Cell(w,h,s[j,sep-j],b,2,align,fill)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
else
-
i+=ascii ? 1 : 2
-
end
-
end
-
#Last chunk
-
if(border and not border.to_s.index('B').nil?)
-
b+='B'
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
-
# move cursor to specified position
-
if (ln == 1)
-
# go to the beginning of the next line
-
@x=@l_margin
-
elsif (ln == 0)
-
# go to the top-right of the cell
-
@y = prevy;
-
@x = prevx + w;
-
elsif (ln == 2)
-
# go to the bottom-left of the cell
-
@x = prevx;
-
end
-
end
-
-
1
def Write(h,txt,link='',fill=0)
-
if(@current_font['type']=='Type0')
-
MBWrite(h,txt,link,fill)
-
else
-
super(h,txt,link,fill)
-
end
-
end
-
-
1
def MBWrite(h,txt,link,fill=0)
-
#Multi-byte version of Write()
-
cw=@current_font['cw']
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
#Check if ASCII or MB
-
ascii=(c<128)
-
if(c.chr=="\n")
-
#Explicit line break
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
next
-
end
-
if(!ascii or c.chr==' ')
-
sep=i
-
end
-
l+=(ascii ? cw[c.chr] : 1000) || 0
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(@x>@l_margin)
-
#Move to next line
-
@x=@l_margin
-
@y+=h
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
i+=1
-
nl+=1
-
next
-
end
-
if(i==j)
-
i+=ascii ? 1 : 2
-
end
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
else
-
Cell(w,h,s[j,sep-j],0,2,'',fill,link)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
else
-
i+=ascii ? 1 : 2
-
end
-
end
-
#Last chunk
-
if(i!=j)
-
Cell(l*@font_size/1000.0,h,s[j,i-j],0,0,'',fill,link)
-
end
-
end
-
-
1
private
-
-
1
def putfonts()
-
nf=@n
-
@diffs.each do |diff|
-
#Encodings
-
newobj()
-
out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')
-
out('endobj')
-
end
-
# mqr=get_magic_quotes_runtime()
-
# set_magic_quotes_runtime(0)
-
@font_files.each_pair do |file, info|
-
#Font file embedding
-
newobj()
-
@font_files[file]['n']=@n
-
if(defined('FPDF_FONTPATH'))
-
file=FPDF_FONTPATH+file
-
end
-
size=filesize(file)
-
if(!size)
-
Error('Font file not found')
-
end
-
out('<</Length '+size)
-
if(file[-2]=='.z')
-
out('/Filter /FlateDecode')
-
end
-
out('/Length1 '+info['length1'])
-
unless info['length2'].nil?
-
out('/Length2 '+info['length2']+' /Length3 0')
-
end
-
out('>>')
-
f=fopen(file,'rb')
-
putstream(fread(f,size))
-
fclose(f)
-
out('endobj')
-
end
-
#
-
# set_magic_quotes_runtime(mqr)
-
#
-
@fonts.each_pair do |k, font|
-
#Font objects
-
newobj()
-
@fonts[k]['n']=@n
-
out('<</Type /Font')
-
if(font['type']=='Type0')
-
putType0(font)
-
else
-
name=font['name']
-
out('/BaseFont /'+name)
-
if(font['type']=='core')
-
#Standard font
-
out('/Subtype /Type1')
-
if(name!='Symbol' and name!='ZapfDingbats')
-
out('/Encoding /WinAnsiEncoding')
-
end
-
else
-
#Additional font
-
out('/Subtype /'+font['type'])
-
out('/FirstChar 32')
-
out('/LastChar 255')
-
out('/Widths '+(@n+1)+' 0 R')
-
out('/FontDescriptor '+(@n+2)+' 0 R')
-
if(font['enc'])
-
if !font['diff'].nil?
-
out('/Encoding '+(nf+font['diff'])+' 0 R')
-
else
-
out('/Encoding /WinAnsiEncoding')
-
end
-
end
-
end
-
out('>>')
-
out('endobj')
-
if(font['type']!='core')
-
#Widths
-
newobj()
-
cw=font['cw']
-
s='['
-
32.upto(255) do |i|
-
s+=cw[i.chr]+' '
-
end
-
out(s+']')
-
out('endobj')
-
#Descriptor
-
newobj()
-
s='<</Type /FontDescriptor /FontName /'+name
-
font['desc'].each_pair do |k, v|
-
s+=' /'+k+' '+v
-
end
-
file=font['file']
-
if(file)
-
s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@font_files[file]['n']+' 0 R'
-
end
-
out(s+'>>')
-
out('endobj')
-
end
-
end
-
end
-
end
-
-
1
def putType0(font)
-
#Type0
-
out('/Subtype /Type0')
-
out('/BaseFont /'+font['name']+'-'+font['CMap'])
-
out('/Encoding /'+font['CMap'])
-
out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
-
out('>>')
-
out('endobj')
-
#CIDFont
-
newobj()
-
out('<</Type /Font')
-
out('/Subtype /CIDFontType0')
-
out('/BaseFont /'+font['name'])
-
out('/CIDSystemInfo <</Registry '+textstring('Adobe')+' /Ordering '+textstring(font['registry']['ordering'])+' /Supplement '+font['registry']['supplement'].to_s+'>>')
-
out('/FontDescriptor '+(@n+1).to_s+' 0 R')
-
if(font['CMap']=='ETen-B5-H')
-
w='13648 13742 500'
-
elsif(font['CMap']=='GBK-EUC-H')
-
w='814 907 500 7716 [500]'
-
else
-
# ActionController::Base::logger.debug font['cw'].keys.sort.join(' ').to_s
-
# ActionController::Base::logger.debug font['cw'].values.join(' ').to_s
-
w='1 ['
-
font['cw'].keys.sort.each {|key|
-
w+=font['cw'][key].to_s + " "
-
# ActionController::Base::logger.debug key.to_s
-
# ActionController::Base::logger.debug font['cw'][key].to_s
-
}
-
w +=']'
-
end
-
out('/W ['+w+']>>')
-
out('endobj')
-
#Font descriptor
-
newobj()
-
out('<</Type /FontDescriptor')
-
out('/FontName /'+font['name'])
-
out('/Flags 6')
-
out('/FontBBox [0 -200 1000 900]')
-
out('/ItalicAngle 0')
-
out('/Ascent 800')
-
out('/Descent -200')
-
out('/CapHeight 800')
-
out('/StemV 50')
-
out('>>')
-
out('endobj')
-
end
-
end
-
# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
-
# 1.12 contributed by Ed Moss.
-
#
-
# The MIT License
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
#
-
# This is direct port of japanese.php
-
#
-
# Japanese PDF support.
-
#
-
# Usage is as follows:
-
#
-
# require 'fpdf'
-
# require 'chinese'
-
# pdf = FPDF.new
-
# pdf.extend(PDF_Japanese)
-
#
-
# This allows it to be combined with other extensions, such as the bookmark
-
# module.
-
-
1
module PDF_Japanese
-
-
1
SJIS_widths={' ' => 278, '!' => 299, '"' => 353, '#' => 614, '$' => 614, '%' => 721, '&' => 735, '\'' => 216,
-
'(' => 323, ')' => 323, '*' => 449, '+' => 529, ',' => 219, '-' => 306, '.' => 219, '/' => 453, '0' => 614, '1' => 614,
-
'2' => 614, '3' => 614, '4' => 614, '5' => 614, '6' => 614, '7' => 614, '8' => 614, '9' => 614, ':' => 219, ';' => 219,
-
'<' => 529, '=' => 529, '>' => 529, '?' => 486, '@' => 744, 'A' => 646, 'B' => 604, 'C' => 617, 'D' => 681, 'E' => 567,
-
'F' => 537, 'G' => 647, 'H' => 738, 'I' => 320, 'J' => 433, 'K' => 637, 'L' => 566, 'M' => 904, 'N' => 710, 'O' => 716,
-
'P' => 605, 'Q' => 716, 'R' => 623, 'S' => 517, 'T' => 601, 'U' => 690, 'V' => 668, 'W' => 990, 'X' => 681, 'Y' => 634,
-
'Z' => 578, '[' => 316, '\\' => 614, ']' => 316, '^' => 529, '_' => 500, '`' => 387, 'a' => 509, 'b' => 566, 'c' => 478,
-
'd' => 565, 'e' => 503, 'f' => 337, 'g' => 549, 'h' => 580, 'i' => 275, 'j' => 266, 'k' => 544, 'l' => 276, 'm' => 854,
-
'n' => 579, 'o' => 550, 'p' => 578, 'q' => 566, 'r' => 410, 's' => 444, 't' => 340, 'u' => 575, 'v' => 512, 'w' => 760,
-
'x' => 503, 'y' => 529, 'z' => 453, '{' => 326, '|' => 380, '}' => 326, '~' => 387}
-
-
1
def AddCIDFont(family,style,name,cw,cMap,registry)
-
fontkey=family.downcase+style.upcase
-
unless @fonts[fontkey].nil?
-
Error("CID font already added: family style")
-
end
-
i=@fonts.length+1
-
@fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-120,'ut'=>40,'cw'=>cw,
-
'CMap'=>cMap,'registry'=>registry}
-
end
-
-
1
def AddCIDFonts(family,name,cw,cMap,registry)
-
AddCIDFont(family,'',name,cw,cMap,registry)
-
AddCIDFont(family,'B',name+',Bold',cw,cMap,registry)
-
AddCIDFont(family,'I',name+',Italic',cw,cMap,registry)
-
AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry)
-
end
-
-
1
def AddSJISFont(family='SJIS')
-
#Add SJIS font with proportional Latin
-
name='KozMinPro-Regular-Acro'
-
cw=SJIS_widths
-
cMap='90msp-RKSJ-H'
-
registry={'ordering'=>'Japan1','supplement'=>2}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def AddSJIShwFont(family='SJIS-hw')
-
#Add SJIS font with half-width Latin
-
name='KozMinPro-Regular-Acro'
-
32.upto(126) do |i|
-
cw[i.chr]=500
-
end
-
cMap='90ms-RKSJ-H'
-
registry={'ordering'=>'Japan1','supplement'=>2}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def GetStringWidth(s)
-
if(@current_font['type']=='Type0')
-
return GetSJISStringWidth(s)
-
else
-
return super(s)
-
end
-
end
-
-
1
def GetSJISStringWidth(s)
-
#SJIS version of GetStringWidth()
-
l=0
-
cw=@current_font['cw']
-
nb=s.length
-
i=0
-
while(i<nb)
-
o = s[i].is_a?(String) ? s[i].ord : s[i]
-
if(o<128)
-
#ASCII
-
l+=cw[o.chr] if cw[o.chr]
-
i+=1
-
elsif(o>=161 and o<=223)
-
#Half-width katakana
-
l+=500
-
i+=1
-
else
-
#Full-width character
-
l+=1000
-
i+=2
-
end
-
end
-
return l*@font_size/1000
-
end
-
-
1
def MultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
if(@current_font['type']=='Type0')
-
SJISMultiCell(w,h,txt,border,align,fill,ln)
-
else
-
super(w,h,txt,border,align,fill,ln)
-
end
-
end
-
-
1
def SJISMultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
-
# save current position
-
prevx = @x;
-
prevy = @y;
-
-
#Output text with automatic or explicit line breaks
-
cw=@current_font['cw']
-
if(w==0)
-
w=@w-@r_margin-@x
-
end
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
if(nb>0 and s[nb-1]=="\n")
-
nb-=1
-
end
-
b=0
-
if(border)
-
if(border==1)
-
border='LTRB'
-
b='LRT'
-
b2='LR'
-
else
-
b2=''
-
b2='L' unless border.to_s.index('L').nil?
-
b2=b2+'R' unless border.to_s.index('R').nil?
-
b=(border.to_s.index('T')) ? (b2+'T') : b2
-
end
-
end
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
o=c #o=ord(c)
-
if(o==10)
-
#Explicit line break
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
next
-
end
-
if(o<128)
-
#ASCII
-
l+=cw[c.chr] || 0
-
n=1
-
if(o==32)
-
sep=i
-
end
-
elsif(o>=161 and o<=223)
-
#Half-width katakana
-
l+=500
-
n=1
-
sep=i
-
else
-
#Full-width character
-
l+=1000
-
n=2
-
sep=i
-
end
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(i==j)
-
i+=n
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
else
-
Cell(w,h,s[j,sep-j],b,2,align,fill)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
else
-
i+=n
-
if(o>=128)
-
sep=i
-
end
-
end
-
end
-
#Last chunk
-
if(border and not border.to_s.index('B').nil?)
-
b+='B'
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
-
# move cursor to specified position
-
if (ln == 1)
-
# go to the beginning of the next line
-
@x=@l_margin
-
elsif (ln == 0)
-
# go to the top-right of the cell
-
@y = prevy;
-
@x = prevx + w;
-
elsif (ln == 2)
-
# go to the bottom-left of the cell
-
@x = prevx;
-
end
-
end
-
-
1
def Write(h,txt,link='',fill=0)
-
if(@current_font['type']=='Type0')
-
SJISWrite(h,txt,link,fill)
-
else
-
super(h,txt,link,fill)
-
end
-
end
-
-
1
def SJISWrite(h,txt,link,fill=0)
-
#SJIS version of Write()
-
cw=@current_font['cw']
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
o=c
-
if(o==10)
-
#Explicit line break
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
#Go to left margin
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
next
-
end
-
if(o<128)
-
#ASCII
-
l+=cw[c.chr] || 0
-
n=1
-
if(o==32)
-
sep=i
-
end
-
elsif(o>=161 and o<=223)
-
#Half-width katakana
-
l+=500
-
n=1
-
sep=i
-
else
-
#Full-width character
-
l+=1000
-
n=2
-
sep=i
-
end
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(@x>@l_margin)
-
#Move to next line
-
@x=@l_margin
-
@y+=h
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
i+=n
-
nl+=1
-
next
-
end
-
if(i==j)
-
i+=n
-
end
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
else
-
Cell(w,h,s[j,sep-j],0,2,'',fill,link)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
else
-
i+=n
-
if(o>=128)
-
sep=i
-
end
-
end
-
end
-
#Last chunk
-
if(i!=j)
-
Cell(l*@font_size/1000.0,h,s[j,i-j],0,0,'',fill,link)
-
end
-
end
-
-
1
private
-
-
1
def putfonts()
-
nf=@n
-
@diffs.each do |diff|
-
#Encodings
-
newobj()
-
out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')
-
out('endobj')
-
end
-
# mqr=get_magic_quotes_runtime()
-
# set_magic_quotes_runtime(0)
-
@font_files.each_pair do |file, info|
-
#Font file embedding
-
newobj()
-
@font_files[file]['n']=@n
-
if(defined('FPDF_FONTPATH'))
-
file=FPDF_FONTPATH+file
-
end
-
size=filesize(file)
-
if(!size)
-
Error('Font file not found')
-
end
-
out('<</Length '+size)
-
if(file[-2]=='.z')
-
out('/Filter /FlateDecode')
-
end
-
out('/Length1 '+info['length1'])
-
unless info['length2'].nil?
-
out('/Length2 '+info['length2']+' /Length3 0')
-
end
-
out('>>')
-
f=fopen(file,'rb')
-
putstream(fread(f,size))
-
fclose(f)
-
out('endobj')
-
end
-
# set_magic_quotes_runtime(mqr)
-
@fonts.each_pair do |k, font|
-
#Font objects
-
newobj()
-
@fonts[k]['n']=@n
-
out('<</Type /Font')
-
if(font['type']=='Type0')
-
putType0(font)
-
else
-
name=font['name']
-
out('/BaseFont /'+name)
-
if(font['type']=='core')
-
#Standard font
-
out('/Subtype /Type1')
-
if(name!='Symbol' and name!='ZapfDingbats')
-
out('/Encoding /WinAnsiEncoding')
-
end
-
else
-
#Additional font
-
out('/Subtype /'+font['type'])
-
out('/FirstChar 32')
-
out('/LastChar 255')
-
out('/Widths '+(@n+1)+' 0 R')
-
out('/FontDescriptor '+(@n+2)+' 0 R')
-
if(font['enc'])
-
if !font['diff'].nil?
-
out('/Encoding '+(nf+font['diff'])+' 0 R')
-
else
-
out('/Encoding /WinAnsiEncoding')
-
end
-
end
-
end
-
out('>>')
-
out('endobj')
-
if(font['type']!='core')
-
#Widths
-
newobj()
-
cw=font['cw']
-
s='['
-
32.upto(255) do |i|
-
s+=cw[i.chr]+' '
-
end
-
out(s+']')
-
out('endobj')
-
#Descriptor
-
newobj()
-
s='<</Type /FontDescriptor /FontName /'+name
-
font['desc'].each_pair do |k, v|
-
s+=' /'+k+' '+v
-
end
-
file=font['file']
-
if(file)
-
s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@font_files[file]['n']+' 0 R'
-
end
-
out(s+'>>')
-
out('endobj')
-
end
-
end
-
end
-
end
-
-
1
def putType0(font)
-
#Type0
-
out('/Subtype /Type0')
-
out('/BaseFont /'+font['name']+'-'+font['CMap'])
-
out('/Encoding /'+font['CMap'])
-
out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
-
out('>>')
-
out('endobj')
-
#CIDFont
-
newobj()
-
out('<</Type /Font')
-
out('/Subtype /CIDFontType0')
-
out('/BaseFont /'+font['name'])
-
out('/CIDSystemInfo <</Registry (Adobe) /Ordering ('+font['registry']['ordering']+') /Supplement '+font['registry']['supplement'].to_s+'>>')
-
out('/FontDescriptor '+(@n+1).to_s+' 0 R')
-
w='/W [1 ['
-
font['cw'].keys.sort.each {|key|
-
w+=font['cw'][key].to_s + " "
-
# ActionController::Base::logger.debug key.to_s
-
# ActionController::Base::logger.debug font['cw'][key].to_s
-
}
-
out(w+'] 231 325 500 631 [500] 326 389 500]')
-
out('>>')
-
out('endobj')
-
#Font descriptor
-
newobj()
-
out('<</Type /FontDescriptor')
-
out('/FontName /'+font['name'])
-
out('/Flags 6')
-
out('/FontBBox [0 -200 1000 900]')
-
out('/ItalicAngle 0')
-
out('/Ascent 800')
-
out('/Descent -200')
-
out('/CapHeight 800')
-
out('/StemV 60')
-
out('>>')
-
out('endobj')
-
end
-
end
-
# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
-
# 1.12 contributed by Ed Moss.
-
#
-
# The MIT License
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
#
-
# This is direct port of korean.php
-
#
-
# Korean PDF support.
-
#
-
# Usage is as follows:
-
#
-
# require 'fpdf'
-
# require 'chinese'
-
# pdf = FPDF.new
-
# pdf.extend(PDF_Korean)
-
#
-
# This allows it to be combined with other extensions, such as the bookmark
-
# module.
-
-
1
module PDF_Korean
-
-
1
UHC_widths={' ' => 333, '!' => 416, '"' => 416, '#' => 833, '$' => 625, '%' => 916, '&' => 833, '\'' => 250,
-
'(' => 500, ')' => 500, '*' => 500, '+' => 833, ',' => 291, '-' => 833, '.' => 291, '/' => 375, '0' => 625, '1' => 625,
-
'2' => 625, '3' => 625, '4' => 625, '5' => 625, '6' => 625, '7' => 625, '8' => 625, '9' => 625, ':' => 333, ';' => 333,
-
'<' => 833, '=' => 833, '>' => 916, '?' => 500, '@' => 1000, 'A' => 791, 'B' => 708, 'C' => 708, 'D' => 750, 'E' => 708,
-
'F' => 666, 'G' => 750, 'H' => 791, 'I' => 375, 'J' => 500, 'K' => 791, 'L' => 666, 'M' => 916, 'N' => 791, 'O' => 750,
-
'P' => 666, 'Q' => 750, 'R' => 708, 'S' => 666, 'T' => 791, 'U' => 791, 'V' => 750, 'W' => 1000, 'X' => 708, 'Y' => 708,
-
'Z' => 666, '[' => 500, '\\' => 375, ']' => 500, '^' => 500, '_' => 500, '`' => 333, 'a' => 541, 'b' => 583, 'c' => 541,
-
'd' => 583, 'e' => 583, 'f' => 375, 'g' => 583, 'h' => 583, 'i' => 291, 'j' => 333, 'k' => 583, 'l' => 291, 'm' => 875,
-
'n' => 583, 'o' => 583, 'p' => 583, 'q' => 583, 'r' => 458, 's' => 541, 't' => 375, 'u' => 583, 'v' => 583, 'w' => 833,
-
'x' => 625, 'y' => 625, 'z' => 500, '{' => 583, '|' => 583, '}' => 583, '~' => 750}
-
-
1
def AddCIDFont(family,style,name,cw,cMap,registry)
-
fontkey=family.downcase+style.upcase
-
unless @fonts[fontkey].nil?
-
Error("Font already added: family style")
-
end
-
i=@fonts.length+1
-
name=name.gsub(' ','')
-
@fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'ut'=>40,'cw'=>cw,
-
'CMap'=>cMap,'registry'=>registry}
-
end
-
-
1
def AddCIDFonts(family,name,cw,cMap,registry)
-
AddCIDFont(family,'',name,cw,cMap,registry)
-
AddCIDFont(family,'B',name+',Bold',cw,cMap,registry)
-
AddCIDFont(family,'I',name+',Italic',cw,cMap,registry)
-
AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry)
-
end
-
-
1
def AddUHCFont(family='UHC',name='HYSMyeongJoStd-Medium-Acro')
-
#Add UHC font with proportional Latin
-
cw=UHC_widths
-
cMap='KSCms-UHC-H'
-
registry={'ordering'=>'Korea1','supplement'=>1}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def AddUHChwFont(family='UHC-hw',name='HYSMyeongJoStd-Medium-Acro')
-
#Add UHC font with half-witdh Latin
-
32.upto(126) do |i|
-
cw[i.chr]=500
-
end
-
cMap='KSCms-UHC-HW-H'
-
registry={'ordering'=>'Korea1','supplement'=>1}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def GetStringWidth(s)
-
if(@current_font['type']=='Type0')
-
return GetMBStringWidth(s)
-
else
-
return super(s)
-
end
-
end
-
-
1
def GetMBStringWidth(s)
-
#Multi-byte version of GetStringWidth()
-
l=0
-
cw=@current_font['cw']
-
nb=s.length
-
i=0
-
while(i<nb)
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
if(c<128)
-
l+=cw[c.chr] if cw[c.chr]
-
i+=1
-
else
-
l+=1000
-
i+=2
-
end
-
end
-
return l*@font_size/1000
-
end
-
-
1
def MultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
if(@current_font['type']=='Type0')
-
MBMultiCell(w,h,txt,border,align,fill,ln)
-
else
-
super(w,h,txt,border,align,fill,ln)
-
end
-
end
-
-
1
def MBMultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
-
# save current position
-
prevx = @x;
-
prevy = @y;
-
-
#Multi-byte version of MultiCell()
-
cw=@current_font['cw']
-
if(w==0)
-
w=@w-@r_margin-@x
-
end
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
if(nb>0 and s[nb-1]=="\n")
-
nb-=1
-
end
-
b=0
-
if(border)
-
if(border==1)
-
border='LTRB'
-
b='LRT'
-
b2='LR'
-
else
-
b2=''
-
b2='L' unless border.to_s.index('L').nil?
-
b2=b2+'R' unless border.to_s.index('R').nil?
-
b=(border.to_s.index('T')) ? (b2+'T') : b2
-
end
-
end
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
#Check if ASCII or MB
-
ascii=(c<128)
-
if(c.chr=="\n")
-
#Explicit line break
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
next
-
end
-
if(!ascii)
-
sep=i
-
ls=l
-
elsif(c.chr==' ')
-
sep=i
-
ls=l
-
end
-
l+=(ascii ? cw[c.chr] : 1000) || 0
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(i==j)
-
i+=ascii ? 1 : 2
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
else
-
Cell(w,h,s[j,sep-j],b,2,align,fill)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
else
-
i+=ascii ? 1 : 2
-
end
-
end
-
#Last chunk
-
if(border and not border.to_s.index('B').nil?)
-
b+='B'
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
-
# move cursor to specified position
-
if (ln == 1)
-
# go to the beginning of the next line
-
@x=@l_margin
-
elsif (ln == 0)
-
# go to the top-right of the cell
-
@y = prevy;
-
@x = prevx + w;
-
elsif (ln == 2)
-
# go to the bottom-left of the cell
-
@x = prevx;
-
end
-
end
-
-
1
def Write(h,txt,link='',fill=0)
-
if(@current_font['type']=='Type0')
-
MBWrite(h,txt,link,fill)
-
else
-
super(h,txt,link,fill)
-
end
-
end
-
-
1
def MBWrite(h,txt,link,fill=0)
-
#Multi-byte version of Write()
-
cw=@current_font['cw']
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
#Check if ASCII or MB
-
ascii=(c<128)
-
if(c.chr=="\n")
-
#Explicit line break
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
next
-
end
-
if(!ascii or c.chr==' ')
-
sep=i
-
end
-
l+=(ascii ? cw[c.chr] : 1000) || 0
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(@x>@l_margin)
-
#Move to next line
-
@x=@l_margin
-
@y+=h
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
i+=1
-
nl+=1
-
next
-
end
-
if(i==j)
-
i+=ascii ? 1 : 2
-
end
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
else
-
Cell(w,h,s[j,sep-j],0,2,'',fill,link)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
else
-
i+=ascii ? 1 : 2
-
end
-
end
-
#Last chunk
-
if(i!=j)
-
Cell(l*@font_size/1000.0,h,s[j,i-j],0,0,'',fill,link)
-
end
-
end
-
-
1
private
-
-
1
def putfonts()
-
nf=@n
-
@diffs.each do |diff|
-
#Encodings
-
newobj()
-
out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')
-
out('endobj')
-
end
-
# mqr=get_magic_quotes_runtime()
-
# set_magic_quotes_runtime(0)
-
@font_files.each_pair do |file, info|
-
#Font file embedding
-
newobj()
-
@font_files[file]['n']=@n
-
if(defined('FPDF_FONTPATH'))
-
file=FPDF_FONTPATH+file
-
end
-
size=filesize(file)
-
if(!size)
-
Error('Font file not found')
-
end
-
out('<</Length '+size)
-
if(file[-2]=='.z')
-
out('/Filter /FlateDecode')
-
end
-
out('/Length1 '+info['length1'])
-
if(not info['length2'].nil?)
-
out('/Length2 '+info['length2']+' /Length3 0')
-
end
-
out('>>')
-
f=fopen(file,'rb')
-
putstream(fread(f,size))
-
fclose(f)
-
out('endobj')
-
end
-
# set_magic_quotes_runtime(mqr)
-
@fonts.each_pair do |k, font|
-
#Font objects
-
newobj()
-
@fonts[k]['n']=@n
-
out('<</Type /Font')
-
if(font['type']=='Type0')
-
putType0(font)
-
else
-
name=font['name']
-
out('/BaseFont /'+name)
-
if(font['type']=='core')
-
#Standard font
-
out('/Subtype /Type1')
-
if(name!='Symbol' and name!='ZapfDingbats')
-
out('/Encoding /WinAnsiEncoding')
-
end
-
else
-
#Additional font
-
out('/Subtype /'+font['type'])
-
out('/FirstChar 32')
-
out('/LastChar 255')
-
out('/Widths '+(@n+1)+' 0 R')
-
out('/FontDescriptor '+(@n+2)+' 0 R')
-
if(font['enc'])
-
if(not font['diff'].nil?)
-
out('/Encoding '+(nf+font['diff'])+' 0 R')
-
else
-
out('/Encoding /WinAnsiEncoding')
-
end
-
end
-
end
-
out('>>')
-
out('endobj')
-
if(font['type']!='core')
-
#Widths
-
newobj()
-
cw=font['cw']
-
s='['
-
32.upto(255) do |i|
-
s+=cw[i.chr]+' '
-
end
-
out(s+']')
-
out('endobj')
-
#Descriptor
-
newobj()
-
s='<</Type /FontDescriptor /FontName /'+name
-
font['desc'].each_pair do |k, v|
-
s+=' /'+k+' '+v
-
end
-
file=font['file']
-
if(file)
-
s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@font_files[file]['n']+' 0 R'
-
end
-
out(s+'>>')
-
out('endobj')
-
end
-
end
-
end
-
end
-
-
1
def putType0(font)
-
#Type0
-
out('/Subtype /Type0')
-
out('/BaseFont /'+font['name']+'-'+font['CMap'])
-
out('/Encoding /'+font['CMap'])
-
out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
-
out('>>')
-
out('endobj')
-
#CIDFont
-
newobj()
-
out('<</Type /Font')
-
out('/Subtype /CIDFontType0')
-
out('/BaseFont /'+font['name'])
-
out('/CIDSystemInfo <</Registry (Adobe) /Ordering ('+font['registry']['ordering']+') /Supplement '+font['registry']['supplement'].to_s+'>>')
-
out('/FontDescriptor '+(@n+1).to_s+' 0 R')
-
if(font['CMap']=='KSCms-UHC-HW-H')
-
w='8094 8190 500'
-
else
-
w='1 ['
-
font['cw'].keys.sort.each {|key|
-
w+=font['cw'][key].to_s + " "
-
# ActionController::Base::logger.debug key.to_s
-
# ActionController::Base::logger.debug font['cw'][key].to_s
-
}
-
w +=']'
-
end
-
out('/W ['+w+']>>')
-
out('endobj')
-
#Font descriptor
-
newobj()
-
out('<</Type /FontDescriptor')
-
out('/FontName /'+font['name'])
-
out('/Flags 6')
-
out('/FontBBox [0 -200 1000 900]')
-
out('/ItalicAngle 0')
-
out('/Ascent 800')
-
out('/Descent -200')
-
out('/CapHeight 800')
-
out('/StemV 50')
-
out('>>')
-
out('endobj')
-
end
-
end
-
# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
-
#
-
# The MIT License
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
-
1
require 'action_controller'
-
1
require 'action_view'
-
-
1
require 'rfpdf/action_controller'
-
1
require 'rfpdf/action_view'
-
-
1
require 'rfpdf/template_handler/compile_support'
-
-
1
require 'rfpdf/template_handlers/base'
-
-
-
1
class ActionController::Base
-
1
include RFPDF::ActionController
-
end
-
-
1
class ActionView::Base
-
1
include RFPDF::ActionView
-
end
-
1
module RFPDF
-
1
module ActionController
-
-
1
DEFAULT_RFPDF_OPTIONS = {:inline=>true}
-
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def rfpdf(options)
-
rfpdf_options = breakdown_rfpdf_options options
-
write_inheritable_hash(:rfpdf, rfpdf_options)
-
end
-
-
1
private
-
-
1
def breakdown_rfpdf_options(options)
-
rfpdf_options = options.dup
-
rfpdf_options
-
end
-
end
-
-
1
def rfpdf(options)
-
@rfpdf_options ||= DEFAULT_RFPDF_OPTIONS.dup
-
@rfpdf_options.merge! options
-
end
-
-
-
1
private
-
-
1
def compute_rfpdf_options
-
@rfpdf_options ||= DEFAULT_RFPDF_OPTIONS.dup
-
@rfpdf_options.merge!(self.class.read_inheritable_attribute(:rfpdf) || {}) {|k,o,n| o}
-
@rfpdf_options
-
end
-
end
-
end
-
-
-
1
module RFPDF
-
1
module ActionView
-
-
1
private
-
1
def _rfpdf_compile_setup(dsl_setup = false)
-
compile_support = RFPDF::TemplateHandler::CompileSupport.new(controller)
-
@rfpdf_options = compile_support.options
-
end
-
-
end
-
end
-
-
# Various mathematical calculations extracted from the PDF::Writer for Ruby gem.
-
# - http://rubyforge.org/projects/ruby-pdf
-
# - Copyright 2003 - 2005 Austin Ziegler.
-
# - Licensed under a MIT-style licence.
-
#
-
-
1
module RFPDF::Math
-
1
PI2 = ::Math::PI * 2.0
-
-
# One degree of arc measured in terms of radians.
-
1
DR = PI2 / 360.0
-
# One radian of arc, measured in terms of degrees.
-
1
RD = 360 / PI2
-
# One degree of arc, measured in terms of gradians.
-
1
DG = 400 / 360.0
-
# One gradian of arc, measured in terms of degrees.
-
1
GD = 360 / 400.0
-
# One radian of arc, measured in terms of gradians.
-
1
RG = 400 / PI2
-
# One gradian of arc, measured in terms of radians.
-
1
GR = PI2 / 400.0
-
-
# Truncate the remainder.
-
1
def remt(num, den)
-
num - den * (num / den.to_f).to_i
-
end
-
-
# Wrap radian values within the range of radians (0..PI2).
-
1
def rad2rad(rad)
-
remt(rad, PI2)
-
end
-
-
# Wrap degree values within the range of degrees (0..360).
-
1
def deg2deg(deg)
-
remt(deg, 360)
-
end
-
-
# Wrap gradian values within the range of gradians (0..400).
-
1
def grad2grad(grad)
-
remt(grad, 400)
-
end
-
-
# Convert degrees to radians. The value will be constrained to the
-
# range of radians (0..PI2) unless +wrap+ is false.
-
1
def deg2rad(deg, wrap = true)
-
rad = DR * deg
-
rad = rad2rad(rad) if wrap
-
rad
-
end
-
-
# Convert degrees to gradians. The value will be constrained to the
-
# range of gradians (0..400) unless +wrap+ is false.
-
1
def deg2grad(deg, wrap = true)
-
grad = DG * deg
-
grad = grad2grad(grad) if wrap
-
grad
-
end
-
-
# Convert radians to degrees. The value will be constrained to the
-
# range of degrees (0..360) unless +wrap+ is false.
-
1
def rad2deg(rad, wrap = true)
-
deg = RD * rad
-
deg = deg2deg(deg) if wrap
-
deg
-
end
-
-
# Convert radians to gradians. The value will be constrained to the
-
# range of gradians (0..400) unless +wrap+ is false.
-
1
def rad2grad(rad, wrap = true)
-
grad = RG * rad
-
grad = grad2grad(grad) if wrap
-
grad
-
end
-
-
# Convert gradians to degrees. The value will be constrained to the
-
# range of degrees (0..360) unless +wrap+ is false.
-
1
def grad2deg(grad, wrap = true)
-
deg = GD * grad
-
deg = deg2deg(deg) if wrap
-
deg
-
end
-
-
# Convert gradians to radians. The value will be constrained to the
-
# range of radians (0..PI2) unless +wrap+ is false.
-
1
def grad2rad(grad, wrap = true)
-
rad = GR * grad
-
rad = rad2rad(rad) if wrap
-
rad
-
end
-
end
-
1
module RFPDF
-
1
module TemplateHandler
-
-
1
class CompileSupport
-
# extend ActiveSupport::Memoizable
-
-
1
attr_reader :options
-
-
1
def initialize(controller)
-
@controller = controller
-
@options = pull_options
-
set_headers
-
end
-
-
1
def pull_options
-
@controller.send :compute_rfpdf_options || {}
-
end
-
-
1
def set_headers
-
set_pragma
-
set_cache_control
-
set_content_type
-
set_disposition
-
end
-
-
# TODO: kept around from railspdf-- maybe not needed anymore? should check.
-
1
def ie_request?
-
@controller.request.env['HTTP_USER_AGENT'] =~ /msie/i
-
end
-
# memoize :ie_request?
-
-
# added to make ie happy with ssl pdf's (per naisayer)
-
1
def ssl_request?
-
# @controller.request.env['SERVER_PROTOCOL'].downcase == "https"
-
@controller.request.ssl?
-
end
-
# memoize :ssl_request?
-
-
# TODO: kept around from railspdf-- maybe not needed anymore? should check.
-
1
def set_pragma
-
if ssl_request? && ie_request?
-
@controller.headers['Pragma'] = 'public' # added to make ie ssl pdfs work (per naisayer)
-
else
-
@controller.headers['Pragma'] ||= ie_request? ? 'no-cache' : ''
-
end
-
end
-
-
# TODO: kept around from railspdf-- maybe not needed anymore? should check.
-
1
def set_cache_control
-
if ssl_request? && ie_request?
-
@controller.headers['Cache-Control'] = 'maxage=1' # added to make ie ssl pdfs work (per naisayer)
-
else
-
@controller.headers['Cache-Control'] ||= ie_request? ? 'no-cache, must-revalidate' : ''
-
end
-
end
-
-
1
def set_content_type
-
@controller.response.content_type ||= Mime::PDF
-
end
-
-
1
def set_disposition
-
inline = options[:inline] ? 'inline' : 'attachment'
-
filename = options[:filename] ? "filename=#{options[:filename]}" : nil
-
@controller.headers["Content-Disposition"] = [inline,filename].compact.join(';')
-
end
-
-
end
-
-
end
-
end
-
-
-
-
1
module RFPDF
-
1
module TemplateHandlers
-
# class Base < ::ActionView::TemplateHandlers::ERB
-
-
# def compile(template)
-
# src = "_rfpdf_compile_setup;" + super
-
# end
-
# end
-
end
-
end
-
-
-
#============================================================+
-
# File name : tcpdf.rb
-
# Begin : 2002-08-03
-
# Last Update : 2007-03-20
-
# Author : Nicola Asuni
-
# Version : 1.53.0.TC031
-
# License : GNU LGPL (http://www.gnu.org/copyleft/lesser.html)
-
#
-
# Description : This is a Ruby class for generating PDF files
-
# on-the-fly without requiring external
-
# extensions.
-
#
-
# IMPORTANT:
-
# This class is an extension and improvement of the Public Domain
-
# FPDF class by Olivier Plathey (http://www.fpdf.org).
-
#
-
# Main changes by Nicola Asuni:
-
# Ruby porting;
-
# UTF-8 Unicode support;
-
# code refactoring;
-
# source code clean up;
-
# code style and formatting;
-
# source code documentation using phpDocumentor (www.phpdoc.org);
-
# All ISO page formats were included;
-
# image scale factor;
-
# includes methods to parse and printsome XHTML code, supporting the following elements: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small;
-
# includes a method to print various barcode formats using an improved version of "Generic Barcode Render Class" by Karim Mribti (http://www.mribti.com/barcode/) (require GD library: http://www.boutell.com/gd/);
-
# defines standard Header() and Footer() methods.
-
#
-
# Ported to Ruby by Ed Moss 2007-08-06
-
#
-
#============================================================+
-
-
1
require 'tempfile'
-
1
require 'core/rmagick'
-
-
#
-
# TCPDF Class.
-
# @package com.tecnick.tcpdf
-
#
-
-
1
@@version = "1.53.0.TC031"
-
1
@@fpdf_charwidths = {}
-
-
1
PDF_PRODUCER = 'TCPDF via RFPDF 1.53.0.TC031 (http://tcpdf.sourceforge.net)'
-
-
1
module TCPDFFontDescriptor
-
1
@@descriptors = { 'freesans' => {} }
-
1
@@font_name = 'freesans'
-
-
1
def self.font(font_name)
-
@@descriptors[font_name.gsub(".rb", "")]
-
end
-
-
1
def self.define(font_name = 'freesans')
-
@@descriptors[font_name] ||= {}
-
yield @@descriptors[font_name]
-
end
-
end
-
-
# This is a Ruby class for generating PDF files on-the-fly without requiring external extensions.<br>
-
# This class is an extension and improvement of the FPDF class by Olivier Plathey (http://www.fpdf.org).<br>
-
# This version contains some changes: [porting to Ruby, support for UTF-8 Unicode, code style and formatting, php documentation (www.phpdoc.org), ISO page formats, minor improvements, image scale factor]<br>
-
# TCPDF project (http://tcpdf.sourceforge.net) is based on the Public Domain FPDF class by Olivier Plathey (http://www.fpdf.org).<br>
-
# To add your own TTF fonts please read /fonts/README.TXT
-
# @name TCPDF
-
# @package com.tecnick.tcpdf
-
# @@version 1.53.0.TC031
-
# @author Nicola Asuni
-
# @link http://tcpdf.sourceforge.net
-
# @license http://www.gnu.org/copyleft/lesser.html LGPL
-
#
-
1
class TCPDF
-
1
include RFPDF
-
1
include Core::RFPDF
-
1
include RFPDF::Math
-
-
1
def logger
-
Rails.logger
-
end
-
-
1
cattr_accessor :k_cell_height_ratio
-
1
@@k_cell_height_ratio = 1.25
-
-
1
cattr_accessor :k_blank_image
-
1
@@k_blank_image = ""
-
-
1
cattr_accessor :k_small_ratio
-
1
@@k_small_ratio = 2/3.0
-
-
1
cattr_accessor :k_path_cache
-
1
@@k_path_cache = Rails.root.join('tmp')
-
-
1
cattr_accessor :k_path_url_cache
-
1
@@k_path_url_cache = Rails.root.join('tmp')
-
-
1
cattr_accessor :decoder
-
-
1
attr_accessor :barcode
-
-
1
attr_accessor :buffer
-
-
1
attr_accessor :diffs
-
-
1
attr_accessor :color_flag
-
-
1
attr_accessor :default_table_columns
-
-
1
attr_accessor :max_table_columns
-
-
1
attr_accessor :default_font
-
-
1
attr_accessor :draw_color
-
-
1
attr_accessor :encoding
-
-
1
attr_accessor :fill_color
-
-
1
attr_accessor :fonts
-
-
1
attr_accessor :font_family
-
-
1
attr_accessor :font_files
-
-
1
cattr_accessor :font_path
-
-
1
attr_accessor :font_style
-
-
1
attr_accessor :font_size_pt
-
-
1
attr_accessor :header_width
-
-
1
attr_accessor :header_logo
-
-
1
attr_accessor :header_logo_width
-
-
1
attr_accessor :header_title
-
-
1
attr_accessor :header_string
-
-
1
attr_accessor :images
-
-
1
attr_accessor :img_scale
-
-
1
attr_accessor :in_footer
-
-
1
attr_accessor :is_unicode
-
-
1
attr_accessor :lasth
-
-
1
attr_accessor :links
-
-
1
attr_accessor :list_ordered
-
-
1
attr_accessor :list_count
-
-
1
attr_accessor :li_spacer
-
-
1
attr_accessor :n
-
-
1
attr_accessor :offsets
-
-
1
attr_accessor :orientation_changes
-
-
1
attr_accessor :page
-
-
1
attr_accessor :page_links
-
-
1
attr_accessor :pages
-
-
1
attr_accessor :pdf_version
-
-
1
attr_accessor :prevfill_color
-
-
1
attr_accessor :prevtext_color
-
-
1
attr_accessor :print_header
-
-
1
attr_accessor :print_footer
-
-
1
attr_accessor :state
-
-
1
attr_accessor :tableborder
-
-
1
attr_accessor :tdbegin
-
-
1
attr_accessor :tdwidth
-
-
1
attr_accessor :tdheight
-
-
1
attr_accessor :tdalign
-
-
1
attr_accessor :tdfill
-
-
1
attr_accessor :tempfontsize
-
-
1
attr_accessor :text_color
-
-
1
attr_accessor :underline
-
-
1
attr_accessor :ws
-
-
#
-
# This is the class constructor.
-
# It allows to set up the page format, the orientation and
-
# the measure unit used in all the methods (except for the font sizes).
-
# @since 1.0
-
# @param string :orientation page orientation. Possible values are (case insensitive):<ul><li>P or Portrait (default)</li><li>L or Landscape</li></ul>
-
# @param string :unit User measure unit. Possible values are:<ul><li>pt: point</li><li>mm: millimeter (default)</li><li>cm: centimeter</li><li>in: inch</li></ul><br />A point equals 1/72 of inch, that is to say about 0.35 mm (an inch being 2.54 cm). This is a very common unit in typography; font sizes are expressed in that unit.
-
# @param mixed :format The format used for pages. It can be either one of the following values (case insensitive) or a custom format in the form of a two-element array containing the width and the height (expressed in the unit given by unit).<ul><li>4A0</li><li>2A0</li><li>A0</li><li>A1</li><li>A2</li><li>A3</li><li>A4 (default)</li><li>A5</li><li>A6</li><li>A7</li><li>A8</li><li>A9</li><li>A10</li><li>B0</li><li>B1</li><li>B2</li><li>B3</li><li>B4</li><li>B5</li><li>B6</li><li>B7</li><li>B8</li><li>B9</li><li>B10</li><li>C0</li><li>C1</li><li>C2</li><li>C3</li><li>C4</li><li>C5</li><li>C6</li><li>C7</li><li>C8</li><li>C9</li><li>C10</li><li>RA0</li><li>RA1</li><li>RA2</li><li>RA3</li><li>RA4</li><li>SRA0</li><li>SRA1</li><li>SRA2</li><li>SRA3</li><li>SRA4</li><li>LETTER</li><li>LEGAL</li><li>EXECUTIVE</li><li>FOLIO</li></ul>
-
# @param boolean :unicode TRUE means that the input text is unicode (default = true)
-
# @param String :encoding charset encoding; default is UTF-8
-
#
-
1
def initialize(orientation = 'P', unit = 'mm', format = 'A4', unicode = true, encoding = "UTF-8")
-
-
# Set internal character encoding to ASCII#
-
#FIXME 2007-05-25 (EJM) Level=0 -
-
# if (respond_to?("mb_internal_encoding") and mb_internal_encoding())
-
# @internal_encoding = mb_internal_encoding();
-
# mb_internal_encoding("ASCII");
-
# }
-
-
#Some checks
-
dochecks();
-
-
begin
-
@@decoder = HTMLEntities.new
-
rescue
-
@@decoder = nil
-
end
-
-
#Initialization of properties
-
@barcode ||= false
-
@buffer ||= ''
-
@diffs ||= []
-
@color_flag ||= false
-
@default_table_columns ||= 4
-
@table_columns ||= 0
-
@max_table_columns ||= []
-
@tr_id ||= 0
-
@max_td_page ||= []
-
@max_td_y ||= []
-
@t_columns ||= 0
-
@default_font ||= "FreeSans" if unicode
-
@default_font ||= "Helvetica"
-
@draw_color ||= '0 G'
-
@encoding ||= "UTF-8"
-
@fill_color ||= '0 g'
-
@fonts ||= {}
-
@font_family ||= ''
-
@font_files ||= {}
-
@font_style ||= ''
-
@font_size ||= 12
-
@font_size_pt ||= 12
-
@header_width ||= 0
-
@header_logo ||= ""
-
@header_logo_width ||= 30
-
@header_title ||= ""
-
@header_string ||= ""
-
@images ||= {}
-
@img_scale ||= 1
-
@in_footer ||= false
-
@is_unicode = unicode
-
@lasth ||= 0
-
@links ||= []
-
@list_ordered ||= []
-
@list_count ||= []
-
@li_spacer ||= ""
-
@li_count ||= 0
-
@spacer ||= ""
-
@quote_count ||= 0
-
@prevquote_count ||= 0
-
@quote_top ||= []
-
@quote_page ||= []
-
@n ||= 2
-
@offsets ||= []
-
@orientation_changes ||= []
-
@page ||= 0
-
@page_links ||= {}
-
@pages ||= []
-
@pdf_version ||= "1.3"
-
@prevfill_color ||= [255,255,255]
-
@prevtext_color ||= [0,0,0]
-
@print_header ||= false
-
@print_footer ||= false
-
@state ||= 0
-
@tableborder ||= 0
-
@tdbegin ||= false
-
@tdtext ||= ''
-
@tdwidth ||= 0
-
@tdheight ||= 0
-
@tdalign ||= "L"
-
@tdfill ||= 0
-
@tempfontsize ||= 10
-
@text_color ||= '0 g'
-
@underline ||= false
-
@deleted ||= false
-
@ws ||= 0
-
-
#Standard Unicode fonts
-
@core_fonts = {
-
'courier'=>'Courier',
-
'courierB'=>'Courier-Bold',
-
'courierI'=>'Courier-Oblique',
-
'courierBI'=>'Courier-BoldOblique',
-
'helvetica'=>'Helvetica',
-
'helveticaB'=>'Helvetica-Bold',
-
'helveticaI'=>'Helvetica-Oblique',
-
'helveticaBI'=>'Helvetica-BoldOblique',
-
'times'=>'Times-Roman',
-
'timesB'=>'Times-Bold',
-
'timesI'=>'Times-Italic',
-
'timesBI'=>'Times-BoldItalic',
-
'symbol'=>'Symbol',
-
'zapfdingbats'=>'ZapfDingbats'}
-
-
#Scale factor
-
case unit.downcase
-
when 'pt' ; @k=1
-
when 'mm' ; @k=72/25.4
-
when 'cm' ; @k=72/2.54
-
when 'in' ; @k=72
-
else Error("Incorrect unit: #{unit}")
-
end
-
-
#Page format
-
if format.is_a?(String)
-
# Page formats (45 standard ISO paper formats and 4 american common formats).
-
# Paper cordinates are calculated in this way: (inches# 72) where (1 inch = 2.54 cm)
-
case (format.upcase)
-
when '4A0' ; format = [4767.87,6740.79]
-
when '2A0' ; format = [3370.39,4767.87]
-
when 'A0' ; format = [2383.94,3370.39]
-
when 'A1' ; format = [1683.78,2383.94]
-
when 'A2' ; format = [1190.55,1683.78]
-
when 'A3' ; format = [841.89,1190.55]
-
when 'A4' ; format = [595.28,841.89] # ; default
-
when 'A5' ; format = [419.53,595.28]
-
when 'A6' ; format = [297.64,419.53]
-
when 'A7' ; format = [209.76,297.64]
-
when 'A8' ; format = [147.40,209.76]
-
when 'A9' ; format = [104.88,147.40]
-
when 'A10' ; format = [73.70,104.88]
-
when 'B0' ; format = [2834.65,4008.19]
-
when 'B1' ; format = [2004.09,2834.65]
-
when 'B2' ; format = [1417.32,2004.09]
-
when 'B3' ; format = [1000.63,1417.32]
-
when 'B4' ; format = [708.66,1000.63]
-
when 'B5' ; format = [498.90,708.66]
-
when 'B6' ; format = [354.33,498.90]
-
when 'B7' ; format = [249.45,354.33]
-
when 'B8' ; format = [175.75,249.45]
-
when 'B9' ; format = [124.72,175.75]
-
when 'B10' ; format = [87.87,124.72]
-
when 'C0' ; format = [2599.37,3676.54]
-
when 'C1' ; format = [1836.85,2599.37]
-
when 'C2' ; format = [1298.27,1836.85]
-
when 'C3' ; format = [918.43,1298.27]
-
when 'C4' ; format = [649.13,918.43]
-
when 'C5' ; format = [459.21,649.13]
-
when 'C6' ; format = [323.15,459.21]
-
when 'C7' ; format = [229.61,323.15]
-
when 'C8' ; format = [161.57,229.61]
-
when 'C9' ; format = [113.39,161.57]
-
when 'C10' ; format = [79.37,113.39]
-
when 'RA0' ; format = [2437.80,3458.27]
-
when 'RA1' ; format = [1729.13,2437.80]
-
when 'RA2' ; format = [1218.90,1729.13]
-
when 'RA3' ; format = [864.57,1218.90]
-
when 'RA4' ; format = [609.45,864.57]
-
when 'SRA0' ; format = [2551.18,3628.35]
-
when 'SRA1' ; format = [1814.17,2551.18]
-
when 'SRA2' ; format = [1275.59,1814.17]
-
when 'SRA3' ; format = [907.09,1275.59]
-
when 'SRA4' ; format = [637.80,907.09]
-
when 'LETTER' ; format = [612.00,792.00]
-
when 'LEGAL' ; format = [612.00,1008.00]
-
when 'EXECUTIVE' ; format = [521.86,756.00]
-
when 'FOLIO' ; format = [612.00,936.00]
-
#else then Error("Unknown page format: #{format}"
-
end
-
@fw_pt = format[0]
-
@fh_pt = format[1]
-
else
-
@fw_pt = format[0]*@k
-
@fh_pt = format[1]*@k
-
end
-
-
@fw = @fw_pt/@k
-
@fh = @fh_pt/@k
-
-
#Page orientation
-
orientation = orientation.downcase
-
if orientation == 'p' or orientation == 'portrait'
-
@def_orientation = 'P'
-
@w_pt = @fw_pt
-
@h_pt = @fh_pt
-
elsif orientation == 'l' or orientation == 'landscape'
-
@def_orientation = 'L'
-
@w_pt = @fh_pt
-
@h_pt = @fw_pt
-
else
-
Error("Incorrect orientation: #{orientation}")
-
end
-
-
@fw = @w_pt/@k
-
@fh = @h_pt/@k
-
-
@cur_orientation = @def_orientation
-
@w = @w_pt/@k
-
@h = @h_pt/@k
-
#Page margins (1 cm)
-
margin = 28.35/@k
-
SetMargins(margin, margin)
-
#Interior cell margin (1 mm)
-
@c_margin = margin / 10
-
#Line width (0.2 mm)
-
@line_width = 0.567 / @k
-
#Automatic page break
-
SetAutoPageBreak(true, 2 * margin)
-
#Full width display mode
-
SetDisplayMode('fullwidth')
-
#Compression
-
SetCompression(true)
-
#Set default PDF version number
-
@pdf_version = "1.3"
-
-
@encoding = encoding
-
@b = 0
-
@i = 0
-
@u = 0
-
@href = ''
-
@fontlist = ["arial", "times", "courier", "helvetica", "symbol"]
-
@issetfont = false
-
@issetcolor = false
-
-
SetFillColor(200, 200, 200, true)
-
SetTextColor(0, 0, 0, true)
-
end
-
-
#
-
# Set the image scale.
-
# @param float :scale image scale.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def SetImageScale(scale)
-
@img_scale = scale;
-
end
-
1
alias_method :set_image_scale, :SetImageScale
-
-
#
-
# Returns the image scale.
-
# @return float image scale.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def GetImageScale()
-
return @img_scale;
-
end
-
1
alias_method :get_image_scale, :GetImageScale
-
-
#
-
# Returns the page width in units.
-
# @return int page width.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def GetPageWidth()
-
return @w;
-
end
-
1
alias_method :get_page_width, :GetPageWidth
-
-
#
-
# Returns the page height in units.
-
# @return int page height.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def GetPageHeight()
-
return @h;
-
end
-
1
alias_method :get_page_height, :GetPageHeight
-
-
#
-
# Returns the page break margin.
-
# @return int page break margin.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def GetBreakMargin()
-
return @b_margin;
-
end
-
1
alias_method :get_break_margin, :GetBreakMargin
-
-
#
-
# Returns the scale factor (number of points in user unit).
-
# @return int scale factor.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def GetScaleFactor()
-
return @k;
-
end
-
1
alias_method :get_scale_factor, :GetScaleFactor
-
-
#
-
# Defines the left, top and right margins. By default, they equal 1 cm. Call this method to change them.
-
# @param float :left Left margin.
-
# @param float :top Top margin.
-
# @param float :right Right margin. Default value is the left one.
-
# @since 1.0
-
# @see SetLeftMargin(), SetTopMargin(), SetRightMargin(), SetAutoPageBreak()
-
#
-
1
def SetMargins(left, top, right=-1)
-
#Set left, top and right margins
-
@l_margin = left
-
@t_margin = top
-
if (right == -1)
-
right = left
-
end
-
@r_margin = right
-
end
-
1
alias_method :set_margins, :SetMargins
-
-
#
-
# Defines the left margin. The method can be called before creating the first page. If the current abscissa gets out of page, it is brought back to the margin.
-
# @param float :margin The margin.
-
# @since 1.4
-
# @see SetTopMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins()
-
#
-
1
def SetLeftMargin(margin)
-
#Set left margin
-
@l_margin = margin
-
if ((@page>0) and (@x < margin))
-
@x = margin
-
end
-
end
-
1
alias_method :set_left_margin, :SetLeftMargin
-
-
#
-
# Defines the top margin. The method can be called before creating the first page.
-
# @param float :margin The margin.
-
# @since 1.5
-
# @see SetLeftMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins()
-
#
-
1
def SetTopMargin(margin)
-
#Set top margin
-
@t_margin = margin
-
end
-
1
alias_method :set_top_margin, :SetTopMargin
-
-
#
-
# Defines the right margin. The method can be called before creating the first page.
-
# @param float :margin The margin.
-
# @since 1.5
-
# @see SetLeftMargin(), SetTopMargin(), SetAutoPageBreak(), SetMargins()
-
#
-
1
def SetRightMargin(margin)
-
#Set right margin
-
@r_margin = margin
-
end
-
1
alias_method :set_right_margin, :SetRightMargin
-
-
#
-
# Enables or disables the automatic page breaking mode. When enabling, the second parameter is the distance from the bottom of the page that defines the triggering limit. By default, the mode is on and the margin is 2 cm.
-
# @param boolean :auto Boolean indicating if mode should be on or off.
-
# @param float :margin Distance from the bottom of the page.
-
# @since 1.0
-
# @see Cell(), MultiCell(), AcceptPageBreak()
-
#
-
1
def SetAutoPageBreak(auto, margin=0)
-
#Set auto page break mode and triggering margin
-
@auto_page_break = auto
-
@b_margin = margin
-
@page_break_trigger = @h - margin
-
end
-
1
alias_method :set_auto_page_break, :SetAutoPageBreak
-
-
#
-
# Defines the way the document is to be displayed by the viewer. The zoom level can be set: pages can be displayed entirely on screen, occupy the full width of the window, use real size, be scaled by a specific zooming factor or use viewer default (configured in the Preferences menu of Acrobat). The page layout can be specified too: single at once, continuous display, two columns or viewer default. By default, documents use the full width mode with continuous display.
-
# @param mixed :zoom The zoom to use. It can be one of the following string values or a number indicating the zooming factor to use. <ul><li>fullpage: displays the entire page on screen </li><li>fullwidth: uses maximum width of window</li><li>real: uses real size (equivalent to 100% zoom)</li><li>default: uses viewer default mode</li></ul>
-
# @param string :layout The page layout. Possible values are:<ul><li>single: displays one page at once</li><li>continuous: displays pages continuously (default)</li><li>two: displays two pages on two columns</li><li>default: uses viewer default mode</li></ul>
-
# @since 1.2
-
#
-
1
def SetDisplayMode(zoom, layout = 'continuous')
-
#Set display mode in viewer
-
if (zoom == 'fullpage' or zoom == 'fullwidth' or zoom == 'real' or zoom == 'default' or !zoom.is_a?(String))
-
@zoom_mode = zoom
-
else
-
Error("Incorrect zoom display mode: #{zoom}")
-
end
-
if (layout == 'single' or layout == 'continuous' or layout == 'two' or layout == 'default')
-
@layout_mode = layout
-
else
-
Error("Incorrect layout display mode: #{layout}")
-
end
-
end
-
1
alias_method :set_display_mode, :SetDisplayMode
-
-
#
-
# Activates or deactivates page compression. When activated, the internal representation of each page is compressed, which leads to a compression ratio of about 2 for the resulting document. Compression is on by default.
-
# Note: the Zlib extension is required for this feature. If not present, compression will be turned off.
-
# @param boolean :compress Boolean indicating if compression must be enabled.
-
# @since 1.4
-
#
-
1
def SetCompression(compress)
-
#Set page compression
-
if (respond_to?('gzcompress'))
-
@compress = compress
-
else
-
@compress = false
-
end
-
end
-
1
alias_method :set_compression, :SetCompression
-
-
#
-
# Defines the title of the document.
-
# @param string :title The title.
-
# @since 1.2
-
# @see SetAuthor(), SetCreator(), SetKeywords(), SetSubject()
-
#
-
1
def SetTitle(title)
-
#Title of document
-
@title = title
-
end
-
1
alias_method :set_title, :SetTitle
-
-
#
-
# Defines the subject of the document.
-
# @param string :subject The subject.
-
# @since 1.2
-
# @see SetAuthor(), SetCreator(), SetKeywords(), SetTitle()
-
#
-
1
def SetSubject(subject)
-
#Subject of document
-
@subject = subject
-
end
-
1
alias_method :set_subject, :SetSubject
-
-
#
-
# Defines the author of the document.
-
# @param string :author The name of the author.
-
# @since 1.2
-
# @see SetCreator(), SetKeywords(), SetSubject(), SetTitle()
-
#
-
1
def SetAuthor(author)
-
#Author of document
-
@author = author
-
end
-
1
alias_method :set_author, :SetAuthor
-
-
#
-
# Associates keywords with the document, generally in the form 'keyword1 keyword2 ...'.
-
# @param string :keywords The list of keywords.
-
# @since 1.2
-
# @see SetAuthor(), SetCreator(), SetSubject(), SetTitle()
-
#
-
1
def SetKeywords(keywords)
-
#Keywords of document
-
@keywords = keywords
-
end
-
1
alias_method :set_keywords, :SetKeywords
-
-
#
-
# Defines the creator of the document. This is typically the name of the application that generates the PDF.
-
# @param string :creator The name of the creator.
-
# @since 1.2
-
# @see SetAuthor(), SetKeywords(), SetSubject(), SetTitle()
-
#
-
1
def SetCreator(creator)
-
#Creator of document
-
@creator = creator
-
end
-
1
alias_method :set_creator, :SetCreator
-
-
#
-
# Defines an alias for the total number of pages. It will be substituted as the document is closed.<br />
-
# <b>Example:</b><br />
-
# <pre>
-
# class PDF extends TCPDF {
-
# def Footer()
-
# #Go to 1.5 cm from bottom
-
# SetY(-15);
-
# #Select Arial italic 8
-
# SetFont('Arial','I',8);
-
# #Print current and total page numbers
-
# Cell(0,10,'Page '.PageNo().'/{nb}',0,0,'C');
-
# end
-
# }
-
# :pdf=new PDF();
-
# :pdf->alias_nb_pages();
-
# </pre>
-
# @param string :alias The alias. Default valuenb}.
-
# @since 1.4
-
# @see PageNo(), Footer()
-
#
-
1
def AliasNbPages(alias_nb ='{nb}')
-
#Define an alias for total number of pages
-
@alias_nb_pages = escapetext(alias_nb)
-
end
-
1
alias_method :alias_nb_pages, :AliasNbPages
-
-
#
-
# This method is automatically called in case of fatal error; it simply outputs the message and halts the execution. An inherited class may override it to customize the error handling but should always halt the script, or the resulting document would probably be invalid.
-
# 2004-06-11 :: Nicola Asuni : changed bold tag with strong
-
# @param string :msg The error message
-
# @since 1.0
-
#
-
1
def Error(msg)
-
#Fatal error
-
raise ("TCPDF error: #{msg}")
-
end
-
1
alias_method :error, :Error
-
-
#
-
# This method begins the generation of the PDF document. It is not necessary to call it explicitly because AddPage() does it automatically.
-
# Note: no page is created by this method
-
# @since 1.0
-
# @see AddPage(), Close()
-
#
-
1
def Open()
-
#Begin document
-
@state = 1
-
end
-
# alias_method :open, :Open
-
-
#
-
# Terminates the PDF document. It is not necessary to call this method explicitly because Output() does it automatically. If the document contains no page, AddPage() is called to prevent from getting an invalid document.
-
# @since 1.0
-
# @see Open(), Output()
-
#
-
1
def Close()
-
#Terminate document
-
if (@state==3)
-
return;
-
end
-
if (@page==0)
-
AddPage();
-
end
-
#Page footer
-
@in_footer=true;
-
Footer();
-
@in_footer=false;
-
#Close page
-
endpage();
-
#Close document
-
enddoc();
-
end
-
# alias_method :close, :Close
-
-
#
-
# Adds a new page to the document. If a page is already present, the Footer() method is called first to output the footer. Then the page is added, the current position set to the top-left corner according to the left and top margins, and Header() is called to display the header.
-
# The font which was set before calling is automatically restored. There is no need to call SetFont() again if you want to continue with the same font. The same is true for colors and line width.
-
# The origin of the coordinate system is at the top-left corner and increasing ordinates go downwards.
-
# @param string :orientation Page orientation. Possible values are (case insensitive):<ul><li>P or Portrait</li><li>L or Landscape</li></ul> The default value is the one passed to the constructor.
-
# @since 1.0
-
# @see TCPDF(), Header(), Footer(), SetMargins()
-
#
-
1
def AddPage(orientation='')
-
#Start a new page
-
if (@state==0)
-
Open();
-
end
-
family=@font_family;
-
style=@font_style + (@underline ? 'U' : '') + (@deleted ? 'D' : '');
-
size=@font_size_pt;
-
lw=@line_width;
-
dc=@draw_color;
-
fc=@fill_color;
-
tc=@text_color;
-
cf=@color_flag;
-
if (@page>0)
-
#Page footer
-
@in_footer=true;
-
Footer();
-
@in_footer=false;
-
#Close page
-
endpage();
-
end
-
#Start new page
-
beginpage(orientation);
-
#Set line cap style to square
-
out('2 J');
-
#Set line width
-
@line_width = lw;
-
out(sprintf('%.2f w', lw*@k));
-
#Set font
-
if (family)
-
SetFont(family, style, size);
-
end
-
#Set colors
-
@draw_color = dc;
-
if (dc!='0 G')
-
out(dc);
-
end
-
@fill_color = fc;
-
if (fc!='0 g')
-
out(fc);
-
end
-
@text_color = tc;
-
@color_flag = cf;
-
#Page header
-
Header();
-
#Restore line width
-
if (@line_width != lw)
-
@line_width = lw;
-
out(sprintf('%.2f w', lw*@k));
-
end
-
#Restore font
-
if (family)
-
SetFont(family, style, size);
-
end
-
#Restore colors
-
if (@draw_color != dc)
-
@draw_color = dc;
-
out(dc);
-
end
-
if (@fill_color != fc)
-
@fill_color = fc;
-
out(fc);
-
end
-
@text_color = tc;
-
@color_flag = cf;
-
end
-
1
alias_method :add_page, :AddPage
-
-
#
-
# Rotate object.
-
# @param float :angle angle in degrees for counter-clockwise rotation
-
# @param int :x abscissa of the rotation center. Default is current x position
-
# @param int :y ordinate of the rotation center. Default is current y position
-
#
-
1
def Rotate(angle, x="", y="")
-
-
if (x == '')
-
x = @x;
-
end
-
-
if (y == '')
-
y = @y;
-
end
-
-
if (@rtl)
-
x = @w - x;
-
angle = -@angle;
-
end
-
-
y = (@h - y) * @k;
-
x *= @k;
-
-
# calculate elements of transformation matrix
-
tm = []
-
tm[0] = ::Math::cos(deg2rad(angle));
-
tm[1] = ::Math::sin(deg2rad(angle));
-
tm[2] = -tm[1];
-
tm[3] = tm[0];
-
tm[4] = x + tm[1] * y - tm[0] * x;
-
tm[5] = y - tm[0] * y - tm[1] * x;
-
-
# generate the transformation matrix
-
Transform(tm);
-
end
-
1
alias_method :rotate, :Rotate
-
-
#
-
# Starts a 2D tranformation saving current graphic state.
-
# This function must be called before scaling, mirroring, translation, rotation and skewing.
-
# Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior.
-
#
-
1
def StartTransform
-
out('q');
-
end
-
1
alias_method :start_transform, :StartTransform
-
-
#
-
# Stops a 2D tranformation restoring previous graphic state.
-
# This function must be called after scaling, mirroring, translation, rotation and skewing.
-
# Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior.
-
#
-
1
def StopTransform
-
out('Q');
-
end
-
1
alias_method :stop_transform, :StopTransform
-
-
#
-
# Apply graphic transformations.
-
# @since 2.1.000 (2008-01-07)
-
# @see StartTransform(), StopTransform()
-
#
-
1
def Transform(tm)
-
x = out(sprintf('%.3f %.3f %.3f %.3f %.3f %.3f cm', tm[0], tm[1], tm[2], tm[3], tm[4], tm[5]));
-
end
-
1
alias_method :transform, :Transform
-
-
#
-
# Set header data.
-
# @param string :ln header image logo
-
# @param string :lw header image logo width in mm
-
# @param string :ht string to print as title on document header
-
# @param string :hs string to print on document header
-
#
-
1
def SetHeaderData(ln="", lw=0, ht="", hs="")
-
@header_logo = ln || ""
-
@header_logo_width = lw || 0
-
@header_title = ht || ""
-
@header_string = hs || ""
-
end
-
1
alias_method :set_header_data, :SetHeaderData
-
-
#
-
# Set header margin.
-
# (minimum distance between header and top page margin)
-
# @param int :hm distance in millimeters
-
#
-
1
def SetHeaderMargin(hm=10)
-
@header_margin = hm;
-
end
-
1
alias_method :set_header_margin, :SetHeaderMargin
-
-
#
-
# Set footer margin.
-
# (minimum distance between footer and bottom page margin)
-
# @param int :fm distance in millimeters
-
#
-
1
def SetFooterMargin(fm=10)
-
@footer_margin = fm;
-
end
-
1
alias_method :set_footer_margin, :SetFooterMargin
-
-
#
-
# Set a flag to print page header.
-
# @param boolean :val set to true to print the page header (default), false otherwise.
-
#
-
1
def SetPrintHeader(val=true)
-
@print_header = val;
-
end
-
1
alias_method :set_print_header, :SetPrintHeader
-
-
#
-
# Set a flag to print page footer.
-
# @param boolean :value set to true to print the page footer (default), false otherwise.
-
#
-
1
def SetPrintFooter(val=true)
-
@print_footer = val;
-
end
-
1
alias_method :set_print_footer, :SetPrintFooter
-
-
#
-
# This method is used to render the page header.
-
# It is automatically called by AddPage() and could be overwritten in your own inherited class.
-
#
-
1
def Header()
-
if (@print_header)
-
if (@original_l_margin.nil?)
-
@original_l_margin = @l_margin;
-
end
-
if (@original_r_margin.nil?)
-
@original_r_margin = @r_margin;
-
end
-
-
#set current position
-
SetXY(@original_l_margin, @header_margin);
-
-
if ((@header_logo) and (@header_logo != @@k_blank_image))
-
Image(@header_logo, @original_l_margin, @header_margin, @header_logo_width);
-
else
-
@img_rb_y = GetY();
-
end
-
-
cell_height = ((@@k_cell_height_ratio * @header_font[2]) / @k).round(2)
-
-
header_x = @original_l_margin + (@header_logo_width * 1.05); #set left margin for text data cell
-
-
# header title
-
SetFont(@header_font[0], 'B', @header_font[2] + 1);
-
SetX(header_x);
-
Cell(@header_width, cell_height, @header_title, 0, 1, 'L');
-
-
# header string
-
SetFont(@header_font[0], @header_font[1], @header_font[2]);
-
SetX(header_x);
-
MultiCell(@header_width, cell_height, @header_string, 0, 'L', 0);
-
-
# print an ending header line
-
if (@header_width)
-
#set style for cell border
-
SetLineWidth(0.3);
-
SetDrawColor(0, 0, 0);
-
SetY(1 + (@img_rb_y > GetY() ? @img_rb_y : GetY()));
-
SetX(@original_l_margin);
-
Cell(0, 0, '', 'T', 0, 'C');
-
end
-
-
#restore position
-
SetXY(@original_l_margin, @t_margin);
-
end
-
end
-
1
alias_method :header, :Header
-
-
#
-
# This method is used to render the page footer.
-
# It is automatically called by AddPage() and could be overwritten in your own inherited class.
-
#
-
1
def Footer()
-
if (@print_footer)
-
-
if (@original_l_margin.nil?)
-
@original_l_margin = @l_margin;
-
end
-
if (@original_r_margin.nil?)
-
@original_r_margin = @r_margin;
-
end
-
-
#set font
-
SetFont(@footer_font[0], @footer_font[1] , @footer_font[2]);
-
#set style for cell border
-
line_width = 0.3;
-
SetLineWidth(line_width);
-
SetDrawColor(0, 0, 0);
-
-
footer_height = ((@@k_cell_height_ratio * @footer_font[2]) / @k).round; #footer height, was , 2)
-
#get footer y position
-
footer_y = @h - @footer_margin - footer_height;
-
#set current position
-
SetXY(@original_l_margin, footer_y);
-
-
#print document barcode
-
if (@barcode)
-
Ln();
-
barcode_width = ((@w - @original_l_margin - @original_r_margin)).round; #max width
-
writeBarcode(@original_l_margin, footer_y + line_width, barcode_width, footer_height - line_width, "C128B", false, false, 2, @barcode);
-
end
-
-
SetXY(@original_l_margin, footer_y);
-
-
#Print page number
-
Cell(0, footer_height, @l['w_page'] + " " + PageNo().to_s + ' / {nb}', 'T', 0, 'R');
-
end
-
end
-
1
alias_method :footer, :Footer
-
-
#
-
# Returns the current page number.
-
# @return int page number
-
# @since 1.0
-
# @see alias_nb_pages()
-
#
-
1
def PageNo()
-
#Get current page number
-
return @page;
-
end
-
1
alias_method :page_no, :PageNo
-
-
#
-
# Defines the color used for all drawing operations (lines, rectangles and cell borders). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page.
-
# @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255
-
# @param int :g Green component (between 0 and 255)
-
# @param int :b Blue component (between 0 and 255)
-
# @since 1.3
-
# @see SetFillColor(), SetTextColor(), Line(), Rect(), Cell(), MultiCell()
-
#
-
1
def SetDrawColor(r, g=-1, b=-1)
-
#Set color for all stroking operations
-
if ((r==0 and g==0 and b==0) or g==-1)
-
@draw_color=sprintf('%.3f G', r/255.0);
-
else
-
@draw_color=sprintf('%.3f %.3f %.3f RG', r/255.0, g/255.0, b/255.0);
-
end
-
if (@page>0)
-
out(@draw_color);
-
end
-
end
-
1
alias_method :set_draw_color, :SetDrawColor
-
-
#
-
# Defines the color used for all filling operations (filled rectangles and cell backgrounds). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page.
-
# @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255
-
# @param int :g Green component (between 0 and 255)
-
# @param int :b Blue component (between 0 and 255)
-
# @param boolean :storeprev if true stores the RGB array on :prevfill_color variable.
-
# @since 1.3
-
# @see SetDrawColor(), SetTextColor(), Rect(), Cell(), MultiCell()
-
#
-
1
def SetFillColor(r, g=-1, b=-1, storeprev=false)
-
#Set color for all filling operations
-
if ((r==0 and g==0 and b==0) or g==-1)
-
@fill_color=sprintf('%.3f g', r/255.0);
-
else
-
@fill_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0);
-
end
-
@color_flag=(@fill_color!=@text_color);
-
if (@page>0)
-
out(@fill_color);
-
end
-
if (storeprev)
-
# store color as previous value
-
@prevfill_color = [r, g, b]
-
end
-
end
-
1
alias_method :set_fill_color, :SetFillColor
-
-
# This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors
-
1
def SetCmykFillColor(c, m, y, k, storeprev=false)
-
#Set color for all filling operations
-
@fill_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k);
-
@color_flag=(@fill_color!=@text_color);
-
if (storeprev)
-
# store color as previous value
-
@prevtext_color = [c, m, y, k]
-
end
-
if (@page>0)
-
out(@fill_color);
-
end
-
end
-
1
alias_method :set_cmyk_fill_color, :SetCmykFillColor
-
-
#
-
# Defines the color used for text. It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page.
-
# @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255
-
# @param int :g Green component (between 0 and 255)
-
# @param int :b Blue component (between 0 and 255)
-
# @param boolean :storeprev if true stores the RGB array on :prevtext_color variable.
-
# @since 1.3
-
# @see SetDrawColor(), SetFillColor(), Text(), Cell(), MultiCell()
-
#
-
1
def SetTextColor(r, g=-1, b=-1, storeprev=false)
-
#Set color for text
-
if ((r==0 and :g==0 and :b==0) or :g==-1)
-
@text_color=sprintf('%.3f g', r/255.0);
-
else
-
@text_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0);
-
end
-
@color_flag=(@fill_color!=@text_color);
-
if (storeprev)
-
# store color as previous value
-
@prevtext_color = [r, g, b]
-
end
-
end
-
1
alias_method :set_text_color, :SetTextColor
-
-
# This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors
-
1
def SetCmykTextColor(c, m, y, k, storeprev=false)
-
#Set color for text
-
@text_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k);
-
@color_flag=(@fill_color!=@text_color);
-
if (storeprev)
-
# store color as previous value
-
@prevtext_color = [c, m, y, k]
-
end
-
end
-
1
alias_method :set_cmyk_text_color, :SetCmykTextColor
-
-
#
-
# Returns the length of a string in user unit. A font must be selected.<br>
-
# Support UTF-8 Unicode [Nicola Asuni, 2005-01-02]
-
# @param string :s The string whose length is to be computed
-
# @return int
-
# @since 1.2
-
#
-
1
def GetStringWidth(s)
-
#Get width of a string in the current font
-
s = s.to_s;
-
cw = @current_font['cw']
-
w = 0;
-
if (@is_unicode)
-
unicode = UTF8StringToArray(s);
-
unicode.each do |char|
-
if (!cw[char].nil?)
-
w += cw[char];
-
# This should not happen. UTF8StringToArray should guarentee the array is ascii values.
-
# elsif (c!cw[char[0]].nil?)
-
# w += cw[char[0]];
-
# elsif (!cw[char.chr].nil?)
-
# w += cw[char.chr];
-
elsif (!@current_font['desc']['MissingWidth'].nil?)
-
w += @current_font['desc']['MissingWidth']; # set default size
-
else
-
w += 500;
-
end
-
end
-
else
-
s.each_byte do |c|
-
if cw[c.chr]
-
w += cw[c.chr];
-
elsif cw[?c.chr]
-
w += cw[?c.chr]
-
end
-
end
-
end
-
return (w * @font_size / 1000.0);
-
end
-
1
alias_method :get_string_width, :GetStringWidth
-
-
#
-
# Defines the line width. By default, the value equals 0.2 mm. The method can be called before the first page is created and the value is retained from page to page.
-
# @param float :width The width.
-
# @since 1.0
-
# @see Line(), Rect(), Cell(), MultiCell()
-
#
-
1
def SetLineWidth(width)
-
#Set line width
-
@line_width = width;
-
if (@page>0)
-
out(sprintf('%.2f w', width*@k));
-
end
-
end
-
1
alias_method :set_line_width, :SetLineWidth
-
-
#
-
# Draws a line between two points.
-
# @param float :x1 Abscissa of first point
-
# @param float :y1 Ordinate of first point
-
# @param float :x2 Abscissa of second point
-
# @param float :y2 Ordinate of second point
-
# @since 1.0
-
# @see SetLineWidth(), SetDrawColor()
-
#
-
1
def Line(x1, y1, x2, y2)
-
#Draw a line
-
out(sprintf('%.2f %.2f m %.2f %.2f l S', x1 * @k, (@h - y1) * @k, x2 * @k, (@h - y2) * @k));
-
end
-
1
alias_method :line, :Line
-
-
1
def Circle(mid_x, mid_y, radius, style='')
-
mid_y = (@h-mid_y)*@k
-
out(sprintf("q\n")) # postscript content in pdf
-
# init line type etc. with /GSD gs G g (grey) RG rg (RGB) w=line witdh etc.
-
out(sprintf("1 j\n")) # line join
-
# translate ("move") circle to mid_y, mid_y
-
out(sprintf("1 0 0 1 %f %f cm", mid_x, mid_y))
-
kappa = 0.5522847498307933984022516322796
-
# Quadrant 1
-
x_s = 0.0 # 12 o'clock
-
y_s = 0.0 + radius
-
x_e = 0.0 + radius # 3 o'clock
-
y_e = 0.0
-
out(sprintf("%f %f m\n", x_s, y_s)) # move to 12 o'clock
-
# cubic bezier control point 1, start height and kappa * radius to the right
-
bx_e1 = x_s + (radius * kappa)
-
by_e1 = y_s
-
# cubic bezier control point 2, end and kappa * radius above
-
bx_e2 = x_e
-
by_e2 = y_e + (radius * kappa)
-
# draw cubic bezier from current point to x_e/y_e with bx_e1/by_e1 and bx_e2/by_e2 as bezier control points
-
out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e))
-
# Quadrant 2
-
x_s = x_e
-
y_s = y_e # 3 o'clock
-
x_e = 0.0
-
y_e = 0.0 - radius # 6 o'clock
-
bx_e1 = x_s # cubic bezier point 1
-
by_e1 = y_s - (radius * kappa)
-
bx_e2 = x_e + (radius * kappa) # cubic bezier point 2
-
by_e2 = y_e
-
out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e))
-
# Quadrant 3
-
x_s = x_e
-
y_s = y_e # 6 o'clock
-
x_e = 0.0 - radius
-
y_e = 0.0 # 9 o'clock
-
bx_e1 = x_s - (radius * kappa) # cubic bezier point 1
-
by_e1 = y_s
-
bx_e2 = x_e # cubic bezier point 2
-
by_e2 = y_e - (radius * kappa)
-
out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e))
-
# Quadrant 4
-
x_s = x_e
-
y_s = y_e # 9 o'clock
-
x_e = 0.0
-
y_e = 0.0 + radius # 12 o'clock
-
bx_e1 = x_s # cubic bezier point 1
-
by_e1 = y_s + (radius * kappa)
-
bx_e2 = x_e - (radius * kappa) # cubic bezier point 2
-
by_e2 = y_e
-
out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e))
-
if style=='F'
-
op='f'
-
elsif style=='FD' or style=='DF'
-
op='b'
-
else
-
op='s'
-
end
-
out(sprintf("#{op}\n")) # stroke circle, do not fill and close path
-
# for filling etc. b, b*, f, f*
-
out(sprintf("Q\n")) # finish postscript in PDF
-
end
-
1
alias_method :circle, :Circle
-
-
#
-
# Outputs a rectangle. It can be drawn (border only), filled (with no border) or both.
-
# @param float :x Abscissa of upper-left corner
-
# @param float :y Ordinate of upper-left corner
-
# @param float :w Width
-
# @param float :h Height
-
# @param string :style Style of rendering. Possible values are:<ul><li>D or empty string: draw (default)</li><li>F: fill</li><li>DF or FD: draw and fill</li></ul>
-
# @since 1.0
-
# @see SetLineWidth(), SetDrawColor(), SetFillColor()
-
#
-
1
def Rect(x, y, w, h, style='')
-
#Draw a rectangle
-
if (style=='F')
-
op='f';
-
elsif (style=='FD' or style=='DF')
-
op='B';
-
else
-
op='S';
-
end
-
out(sprintf('%.2f %.2f %.2f %.2f re %s', x * @k, (@h - y) * @k, w * @k, -h * @k, op));
-
end
-
1
alias_method :rect, :Rect
-
-
#
-
# Imports a TrueType or Type1 font and makes it available. It is necessary to generate a font definition file first with the makefont.rb utility. The definition file (and the font file itself when embedding) must be present either in the current directory or in the one indicated by FPDF_FONTPATH if the constant is defined. If it could not be found, the error "Could not include font definition file" is generated.
-
# Support UTF-8 Unicode [Nicola Asuni, 2005-01-02].
-
# <b>Example</b>:<br />
-
# <pre>
-
# :pdf->AddFont('Comic','I');
-
# # is equivalent to:
-
# :pdf->AddFont('Comic','I','comici.rb');
-
# </pre>
-
# @param string :family Font family. The name can be chosen arbitrarily. If it is a standard family name, it will override the corresponding font.
-
# @param string :style Font style. Possible values are (case insensitive):<ul><li>empty string: regular (default)</li><li>B: bold</li><li>I: italic</li><li>BI or IB: bold italic</li></ul>
-
# @param string :file The font definition file. By default, the name is built from the family and style, in lower case with no space.
-
# @since 1.5
-
# @see SetFont()
-
#
-
1
def AddFont(family, style='', file='')
-
if (family.empty?)
-
return;
-
end
-
-
#Add a TrueType or Type1 font
-
family = family.downcase
-
if ((!@is_unicode) and (family == 'arial'))
-
family = 'helvetica';
-
end
-
-
style=style.upcase
-
style=style.gsub('U','');
-
style=style.gsub('D','');
-
if (style == 'IB')
-
style = 'BI';
-
end
-
-
fontkey = family + style;
-
# check if the font has been already added
-
if !@fonts[fontkey].nil?
-
return;
-
end
-
-
if (file=='')
-
file = family.gsub(' ', '') + style.downcase + '.rb';
-
end
-
font_file_name = getfontpath(file)
-
if (font_file_name.nil?)
-
# try to load the basic file without styles
-
file = family.gsub(' ', '') + '.rb';
-
font_file_name = getfontpath(file)
-
end
-
if font_file_name.nil?
-
Error("Could not find font #{file}.")
-
end
-
require(getfontpath(file))
-
font_desc = TCPDFFontDescriptor.font(file)
-
-
if (font_desc[:name].nil? and @@fpdf_charwidths.nil?)
-
Error('Could not include font definition file');
-
end
-
-
i = @fonts.length+1;
-
if (@is_unicode)
-
@fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg], 'cMap' => font_desc[:cMap], 'registry' => font_desc[:registry]}
-
@@fpdf_charwidths[fontkey] = font_desc[:cw];
-
else
-
@fonts[fontkey]={'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]}
-
@@fpdf_charwidths[fontkey] = font_desc[:cw];
-
end
-
-
if (!font_desc[:diff].nil? and (!font_desc[:diff].empty?))
-
#Search existing encodings
-
d=0;
-
nb=@diffs.length;
-
1.upto(nb) do |i|
-
if (@diffs[i]== font_desc[:diff])
-
d = i;
-
break;
-
end
-
end
-
if (d==0)
-
d = nb+1;
-
@diffs[d] = font_desc[:diff];
-
end
-
@fonts[fontkey]['diff'] = d;
-
end
-
if (font_desc[:file] and font_desc[:file].length > 0)
-
if (font_desc[:type] == "TrueType") or (font_desc[:type] == "TrueTypeUnicode")
-
@font_files[font_desc[:file]] = {'length1' => font_desc[:originalsize]}
-
else
-
@font_files[font_desc[:file]] = {'length1' => font_desc[:size1], 'length2' => font_desc[:size2]}
-
end
-
end
-
end
-
1
alias_method :add_font, :AddFont
-
-
#
-
# Sets the font used to print character strings. It is mandatory to call this method at least once before printing text or the resulting document would not be valid.
-
# The font can be either a standard one or a font added via the AddFont() method. Standard fonts use Windows encoding cp1252 (Western Europe).
-
# The method can be called before the first page is created and the font is retained from page to page.
-
# If you just wish to change the current font size, it is simpler to call SetFontSize().
-
# Note: for the standard fonts, the font metric files must be accessible. There are three possibilities for this:<ul><li>They are in the current directory (the one where the running script lies)</li><li>They are in one of the directories defined by the include_path parameter</li><li>They are in the directory defined by the FPDF_FONTPATH constant</li></ul><br />
-
# Example for the last case (note the trailing slash):<br />
-
# <pre>
-
# define('FPDF_FONTPATH','/home/www/font/');
-
# require('tcpdf.rb');
-
#
-
# #Times regular 12
-
# :pdf->SetFont('Times');
-
# #Arial bold 14
-
# :pdf->SetFont('Arial','B',14);
-
# #Removes bold
-
# :pdf->SetFont('');
-
# #Times bold, italic and underlined 14
-
# :pdf->SetFont('Times','BIUD');
-
# </pre><br />
-
# If the file corresponding to the requested font is not found, the error "Could not include font metric file" is generated.
-
# @param string :family Family font. It can be either a name defined by AddFont() or one of the standard families (case insensitive):<ul><li>Courier (fixed-width)</li><li>Helvetica or Arial (synonymous; sans serif)</li><li>Times (serif)</li><li>Symbol (symbolic)</li><li>ZapfDingbats (symbolic)</li></ul>It is also possible to pass an empty string. In that case, the current family is retained.
-
# @param string :style Font style. Possible values are (case insensitive):<ul><li>empty string: regular</li><li>B: bold</li><li>I: italic</li><li>U: underline</li></ul>or any combination. The default value is regular. Bold and italic styles do not apply to Symbol and ZapfDingbats
-
# @param float :size Font size in points. The default value is the current size. If no size has been specified since the beginning of the document, the value taken is 12
-
# @since 1.0
-
# @see AddFont(), SetFontSize(), Cell(), MultiCell(), Write()
-
#
-
1
def SetFont(family, style='', size=0)
-
# save previous values
-
@prevfont_family = @font_family;
-
@prevfont_style = @font_style;
-
-
family=family.downcase;
-
if (family=='')
-
family=@font_family;
-
end
-
if ((!@is_unicode) and (family == 'arial'))
-
family = 'helvetica';
-
elsif ((family=="symbol") or (family=="zapfdingbats"))
-
style='';
-
end
-
-
style=style.upcase;
-
-
if (style.include?('U'))
-
@underline=true;
-
style= style.gsub('U','');
-
else
-
@underline=false;
-
end
-
if (style.include?('D'))
-
@deleted=true;
-
style= style.gsub('D','');
-
else
-
@deleted=false;
-
end
-
if (style=='IB')
-
style='BI';
-
end
-
if (size==0)
-
size=@font_size_pt;
-
end
-
-
# try to add font (if not already added)
-
AddFont(family, style);
-
-
#Test if font is already selected
-
if ((@font_family == family) and (@font_style == style) and (@font_size_pt == size))
-
return;
-
end
-
-
fontkey = family + style;
-
style = '' if (@fonts[fontkey].nil? and !@fonts[family].nil?)
-
-
#Test if used for the first time
-
if (@fonts[fontkey].nil?)
-
#Check if one of the standard fonts
-
if (!@core_fonts[fontkey].nil?)
-
if @@fpdf_charwidths[fontkey].nil?
-
#Load metric file
-
file = family;
-
if ((family!='symbol') and (family!='zapfdingbats'))
-
file += style.downcase;
-
end
-
if (getfontpath(file + '.rb').nil?)
-
# try to load the basic file without styles
-
file = family;
-
fontkey = family;
-
end
-
require(getfontpath(file + '.rb'));
-
font_desc = TCPDFFontDescriptor.font(file)
-
if ((@is_unicode and ctg.nil?) or ((!@is_unicode) and (@@fpdf_charwidths[fontkey].nil?)) )
-
Error("Could not include font metric file [" + fontkey + "]: " + getfontpath(file + ".rb"));
-
end
-
end
-
i = @fonts.length + 1;
-
-
if (@is_unicode)
-
@fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg]}
-
@@fpdf_charwidths[fontkey] = font_desc[:cw];
-
else
-
@fonts[fontkey] = {'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]}
-
@@fpdf_charwidths[fontkey] = font_desc[:cw];
-
end
-
else
-
Error('Undefined font: ' + family + ' ' + style);
-
end
-
end
-
#Select it
-
@font_family = family;
-
@font_style = style;
-
@font_size_pt = size;
-
@font_size = size / @k;
-
@current_font = @fonts[fontkey]; # was & may need deep copy?
-
if (@page>0)
-
out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt));
-
end
-
end
-
1
alias_method :set_font, :SetFont
-
-
#
-
# Defines the size of the current font.
-
# @param float :size The size (in points)
-
# @since 1.0
-
# @see SetFont()
-
#
-
1
def SetFontSize(size)
-
#Set font size in points
-
if (@font_size_pt== size)
-
return;
-
end
-
@font_size_pt = size;
-
@font_size = size.to_f / @k;
-
if (@page > 0)
-
out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt));
-
end
-
end
-
1
alias_method :set_font_size, :SetFontSize
-
-
#
-
# Creates a new internal link and returns its identifier. An internal link is a clickable area which directs to another place within the document.<br />
-
# The identifier can then be passed to Cell(), Write(), Image() or Link(). The destination is defined with SetLink().
-
# @since 1.5
-
# @see Cell(), Write(), Image(), Link(), SetLink()
-
#
-
1
def AddLink()
-
#Create a new internal link
-
n=@links.length+1;
-
@links[n]=[0,0];
-
return n;
-
end
-
1
alias_method :add_link, :AddLink
-
-
#
-
# Defines the page and position a link points to
-
# @param int :link The link identifier returned by AddLink()
-
# @param float :y Ordinate of target position; -1 indicates the current position. The default value is 0 (top of page)
-
# @param int :page Number of target page; -1 indicates the current page. This is the default value
-
# @since 1.5
-
# @see AddLink()
-
#
-
1
def SetLink(link, y=0, page=-1)
-
#Set destination of internal link
-
if (y==-1)
-
y=@y;
-
end
-
if (page==-1)
-
page=@page;
-
end
-
@links[link] = [page, y]
-
end
-
1
alias_method :set_link, :SetLink
-
-
#
-
# Puts a link on a rectangular area of the page. Text or image links are generally put via Cell(), Write() or Image(), but this method can be useful for instance to define a clickable area inside an image.
-
# @param float :x Abscissa of the upper-left corner of the rectangle
-
# @param float :y Ordinate of the upper-left corner of the rectangle
-
# @param float :w Width of the rectangle
-
# @param float :h Height of the rectangle
-
# @param mixed :link URL or identifier returned by AddLink()
-
# @since 1.5
-
# @see AddLink(), Cell(), Write(), Image()
-
#
-
1
def Link(x, y, w, h, link)
-
#Put a link on the page
-
@page_links ||= Array.new
-
@page_links[@page] ||= Array.new
-
@page_links[@page].push([x * @k, @h_pt - y * @k, w * @k, h*@k, link]);
-
end
-
1
alias_method :link, :Link
-
-
#
-
# Prints a character string. The origin is on the left of the first charcter, on the baseline. This method allows to place a string precisely on the page, but it is usually easier to use Cell(), MultiCell() or Write() which are the standard methods to print text.
-
# @param float :x Abscissa of the origin
-
# @param float :y Ordinate of the origin
-
# @param string :txt String to print
-
# @since 1.0
-
# @see SetFont(), SetTextColor(), Cell(), MultiCell(), Write()
-
#
-
1
def Text(x, y, txt)
-
#Output a string
-
s=sprintf('BT %.2f %.2f Td (%s) Tj ET', x * @k, (@h-y) * @k, escapetext(txt));
-
if (@underline and (txt!=''))
-
s += ' ' + dolinetxt(x, y, txt);
-
end
-
if (@color_flag)
-
s='q ' + @text_color + ' ' + s + ' Q';
-
end
-
out(s);
-
end
-
1
alias_method :text, :Text
-
-
#
-
# Whenever a page break condition is met, the method is called, and the break is issued or not depending on the returned value. The default implementation returns a value according to the mode selected by SetAutoPageBreak().<br />
-
# This method is called automatically and should not be called directly by the application.<br />
-
# <b>Example:</b><br />
-
# The method is overriden in an inherited class in order to obtain a 3 column layout:<br />
-
# <pre>
-
# class PDF extends TCPDF {
-
# var :col=0;
-
#
-
# def SetCol(col)
-
# #Move position to a column
-
# @col = col;
-
# :x=10+:col*65;
-
# SetLeftMargin(x);
-
# SetX(x);
-
# end
-
#
-
# def AcceptPageBreak()
-
# if (@col<2)
-
# #Go to next column
-
# SetCol(@col+1);
-
# SetY(10);
-
# return false;
-
# end
-
# else
-
# #Go back to first column and issue page break
-
# SetCol(0);
-
# return true;
-
# end
-
# end
-
# }
-
#
-
# :pdf=new PDF();
-
# :pdf->Open();
-
# :pdf->AddPage();
-
# :pdf->SetFont('Arial','',12);
-
# for(i=1;:i<=300;:i++)
-
# :pdf->Cell(0,5,"Line :i",0,1);
-
# }
-
# :pdf->Output();
-
# </pre>
-
# @return boolean
-
# @since 1.4
-
# @see SetAutoPageBreak()
-
#
-
1
def AcceptPageBreak()
-
#Accept automatic page break or not
-
return @auto_page_break;
-
end
-
1
alias_method :accept_page_break, :AcceptPageBreak
-
-
1
def BreakThePage?(h)
-
if ((@y + h) > @page_break_trigger and !@in_footer and AcceptPageBreak())
-
true
-
else
-
false
-
end
-
end
-
1
alias_method :break_the_page?, :BreakThePage?
-
#
-
# Prints a cell (rectangular area) with optional borders, background color and character string. The upper-left corner of the cell corresponds to the current position. The text can be aligned or centered. After the call, the current position moves to the right or to the next line. It is possible to put a link on the text.<br />
-
# If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting.
-
# @param float :w Cell width. If 0, the cell extends up to the right margin.
-
# @param float :h Cell height. Default value: 0.
-
# @param string :txt String to print. Default value: empty string.
-
# @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:<ul><li>0: no border (default)</li><li>1: frame</li></ul>or a string containing some or all of the following characters (in any order):<ul><li>L: left</li><li>T: top</li><li>R: right</li><li>B: bottom</li></ul>
-
# @param int :ln Indicates where the current position should go after the call. Possible values are:<ul><li>0: to the right</li><li>1: to the beginning of the next line</li><li>2: below</li></ul>
-
# Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0.
-
# @param string :align Allows to center or align the text. Possible values are:<ul><li>L or empty string: left align (default value)</li><li>C: center</li><li>R: right align</li></ul>
-
# @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0.
-
# @param mixed :link URL or identifier returned by AddLink().
-
# @since 1.0
-
# @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), AddLink(), Ln(), MultiCell(), Write(), SetAutoPageBreak()
-
#
-
1
def Cell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil)
-
#Output a cell
-
k=@k;
-
if ((@y + h) > @page_break_trigger and !@in_footer and AcceptPageBreak())
-
#Automatic page break
-
if @pages[@page+1].nil?
-
x = @x;
-
ws = @ws;
-
if (ws > 0)
-
@ws = 0;
-
out('0 Tw');
-
end
-
AddPage(@cur_orientation);
-
@x = x;
-
if (ws > 0)
-
@ws = ws;
-
out(sprintf('%.3f Tw', ws * k));
-
end
-
else
-
@page += 1;
-
@y=@t_margin;
-
end
-
end
-
-
if (w == 0)
-
w = @w - @r_margin - @x;
-
end
-
s = '';
-
if ((fill.to_i == 1) or (border.to_i == 1))
-
if (fill.to_i == 1)
-
op = (border.to_i == 1) ? 'B' : 'f';
-
else
-
op = 'S';
-
end
-
s = sprintf('%.2f %.2f %.2f %.2f re %s ', @x * k, (@h - @y) * k, w * k, -h * k, op);
-
end
-
if (border.is_a?(String))
-
x=@x;
-
y=@y;
-
if (border.include?('L'))
-
s<<sprintf('%.2f %.2f m %.2f %.2f l S ', x*k,(@h-y)*k, x*k,(@h-(y+h))*k);
-
end
-
if (border.include?('T'))
-
s<<sprintf('%.2f %.2f m %.2f %.2f l S ', x*k,(@h-y)*k,(x+w)*k,(@h-y)*k);
-
end
-
if (border.include?('R'))
-
s<<sprintf('%.2f %.2f m %.2f %.2f l S ',(x+w)*k,(@h-y)*k,(x+w)*k,(@h-(y+h))*k);
-
end
-
if (border.include?('B'))
-
s<<sprintf('%.2f %.2f m %.2f %.2f l S ', x*k,(@h-(y+h))*k,(x+w)*k,(@h-(y+h))*k);
-
end
-
end
-
if (txt != '')
-
width = GetStringWidth(txt);
-
if (align == 'R' || align == 'right')
-
dx = w - @c_margin - width;
-
elsif (align=='C' || align == 'center')
-
dx = (w - width)/2;
-
else
-
dx = @c_margin;
-
end
-
if (@color_flag)
-
s << 'q ' + @text_color + ' ';
-
end
-
txt2 = escapetext(txt);
-
s<<sprintf('BT %.2f %.2f Td (%s) Tj ET', (@x + dx) * k, (@h - (@y + 0.5 * h + 0.3 * @font_size)) * k, txt2);
-
if (@underline)
-
s<<' ' + dolinetxt(@x + dx, @y + 0.5 * h + 0.3 * @font_size, txt);
-
end
-
if (@deleted)
-
s<<' ' + dolinetxt(@x + dx, @y + 0.3 * h + 0.2 * @font_size, txt);
-
end
-
if (@color_flag)
-
s<<' Q';
-
end
-
if link && !link.empty?
-
Link(@x + dx, @y + 0.5 * h - 0.5 * @font_size, width, @font_size, link);
-
end
-
end
-
if (s)
-
out(s);
-
end
-
@lasth = h;
-
if (ln.to_i>0)
-
# Go to next line
-
@y += h;
-
if (ln == 1)
-
@x = @l_margin;
-
end
-
else
-
@x += w;
-
end
-
end
-
1
alias_method :cell, :Cell
-
-
#
-
# This method allows printing text with line breaks. They can be automatic (as soon as the text reaches the right border of the cell) or explicit (via the \n character). As many cells as necessary are output, one below the other.<br />
-
# Text can be aligned, centered or justified. The cell block can be framed and the background painted.
-
# @param float :w Width of cells. If 0, they extend up to the right margin of the page.
-
# @param float :h Height of cells.
-
# @param string :txt String to print
-
# @param mixed :border Indicates if borders must be drawn around the cell block. The value can be either a number:<ul><li>0: no border (default)</li><li>1: frame</li></ul>or a string containing some or all of the following characters (in any order):<ul><li>L: left</li><li>T: top</li><li>R: right</li><li>B: bottom</li></ul>
-
# @param string :align Allows to center or align the text. Possible values are:<ul><li>L or empty string: left align</li><li>C: center</li><li>R: right align</li><li>J: justification (default value)</li></ul>
-
# @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0.
-
# @param int :ln Indicates where the current position should go after the call. Possible values are:<ul><li>0: to the right</li><li>1: to the beginning of the next line [DEFAULT]</li><li>2: below</li></ul>
-
# @since 1.3
-
# @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), Cell(), Write(), SetAutoPageBreak()
-
#
-
1
def MultiCell(w, h, txt, border=0, align='J', fill=0, ln=1)
-
-
# save current position
-
prevx = @x;
-
prevy = @y;
-
prevpage = @page;
-
-
#Output text with automatic or explicit line breaks
-
-
if (w == 0)
-
w = @w - @r_margin - @x;
-
end
-
-
wmax = (w - 3 * @c_margin);
-
-
s = txt.gsub("\r", ''); # remove carriage returns
-
nb = s.length;
-
-
b=0;
-
if (border)
-
if (border==1)
-
border='LTRB';
-
b='LRT';
-
b2='LR';
-
elsif border.is_a?(String)
-
b2='';
-
if (border.include?('L'))
-
b2<<'L';
-
end
-
if (border.include?('R'))
-
b2<<'R';
-
end
-
b=(border.include?('T')) ? b2 + 'T' : b2;
-
end
-
end
-
sep=-1;
-
to_index=0;
-
from_j=0;
-
l=0;
-
ns=0;
-
nl=1;
-
-
while to_index < nb
-
#Get next character
-
c = s[to_index];
-
if c == "\n"[0]
-
#Explicit line break
-
if @ws > 0
-
@ws = 0
-
out('0 Tw')
-
end
-
#Ed Moss - change begin
-
end_i = to_index == 0 ? 0 : to_index - 1
-
# Changed from s[from_j..to_index] to fix bug reported by Hans Allis.
-
from_j = to_index == 0 ? 1 : from_j
-
Cell(w, h, s[from_j..end_i], b, 2, align, fill)
-
#change end
-
to_index += 1
-
sep=-1
-
from_j=to_index
-
l=0
-
ns=0
-
nl += 1
-
b = b2 if border and nl==2
-
next
-
end
-
if (c == " "[0])
-
sep = to_index;
-
ls = l;
-
ns += 1;
-
end
-
-
l = GetStringWidth(s[from_j, to_index - from_j]);
-
-
if (l > wmax)
-
#Automatic line break
-
if (sep == -1)
-
if (to_index == from_j)
-
to_index += 1;
-
end
-
if (@ws > 0)
-
@ws = 0;
-
out('0 Tw');
-
end
-
Cell(w, h, s[from_j..to_index-1], b, 2, align, fill) # my FPDF version
-
else
-
if (align=='J' || align=='justify' || align=='justified')
-
@ws = (ns>1) ? (wmax-ls)/(ns-1) : 0;
-
out(sprintf('%.3f Tw', @ws * @k));
-
end
-
Cell(w, h, s[from_j..sep], b, 2, align, fill);
-
to_index = sep + 1;
-
end
-
sep=-1;
-
from_j = to_index;
-
l=0;
-
ns=0;
-
nl += 1;
-
if (border and (nl==2))
-
b = b2;
-
end
-
else
-
to_index += 1;
-
end
-
end
-
#Last chunk
-
if (@ws>0)
-
@ws=0;
-
out('0 Tw');
-
end
-
if (border.is_a?(String) and border.include?('B'))
-
b<<'B';
-
end
-
Cell(w, h, s[from_j, to_index-from_j], b, 2, align, fill);
-
-
# move cursor to specified position
-
# since 2007-03-03
-
if (ln == 1)
-
# go to the beginning of the next line
-
@x = @l_margin;
-
elsif (ln == 0)
-
# go to the top-right of the cell
-
@page = prevpage;
-
@y = prevy;
-
@x = prevx + w;
-
elsif (ln == 2)
-
# go to the bottom-left of the cell
-
@x = prevx;
-
end
-
end
-
1
alias_method :multi_cell, :MultiCell
-
-
#
-
# This method prints text from the current position. When the right margin is reached (or the \n character is met) a line break occurs and text continues from the left margin. Upon method exit, the current position is left just at the end of the text. It is possible to put a link on the text.<br />
-
# <b>Example:</b><br />
-
# <pre>
-
# #Begin with regular font
-
# :pdf->SetFont('Arial','',14);
-
# :pdf->Write(5,'Visit ');
-
# #Then put a blue underlined link
-
# :pdf->SetTextColor(0,0,255);
-
# :pdf->SetFont('','U');
-
# :pdf->Write(5,'www.tecnick.com','http://www.tecnick.com');
-
# </pre>
-
# @param float :h Line height
-
# @param string :txt String to print
-
# @param mixed :link URL or identifier returned by AddLink()
-
# @param int :fill Indicates if the background must be painted (1) or transparent (0). Default value: 0.
-
# @since 1.5
-
# @see SetFont(), SetTextColor(), AddLink(), MultiCell(), SetAutoPageBreak()
-
#
-
1
def Write(h, txt, link=nil, fill=0)
-
-
#Output text in flowing mode
-
w = @w - @r_margin - @x;
-
wmax = (w - 3 * @c_margin);
-
-
s = txt.gsub("\r", '');
-
nb = s.length;
-
-
# handle single space character
-
if ((nb==1) and (s == " "))
-
@x += GetStringWidth(s);
-
return;
-
end
-
-
sep=-1;
-
i=0;
-
j=0;
-
l=0;
-
nl=1;
-
while(i<nb)
-
#Get next character
-
c = s[i];
-
if (c == "\n"[0])
-
#Explicit line break
-
Cell(w, h, s[j,i-j], 0, 2, '', fill, link);
-
i += 1;
-
sep = -1;
-
j = i;
-
l = 0;
-
if (nl == 1)
-
@x = @l_margin;
-
w = @w - @r_margin - @x;
-
wmax = (w - 3 * @c_margin);
-
end
-
nl += 1;
-
next
-
end
-
if (c == " "[0])
-
sep= i;
-
end
-
l = GetStringWidth(s[j, i - j]);
-
if (l > wmax)
-
#Automatic line break (word wrapping)
-
if (sep == -1)
-
if (@x > @l_margin)
-
#Move to next line
-
@x = @l_margin;
-
@y += h;
-
w=@w - @r_margin - @x;
-
wmax=(w - 3 * @c_margin);
-
i += 1
-
nl += 1
-
next
-
end
-
if (i == j)
-
i += 1
-
end
-
Cell(w, h, s[j, (i-1)], 0, 2, '', fill, link);
-
else
-
Cell(w, h, s[j, (sep-j)], 0, 2, '', fill, link);
-
i = sep+1;
-
end
-
sep = -1;
-
j = i;
-
l = 0;
-
if (nl==1)
-
@x = @l_margin;
-
w = @w - @r_margin - @x;
-
wmax = (w - 3 * @c_margin);
-
end
-
nl += 1;
-
else
-
i += 1;
-
end
-
end
-
#Last chunk
-
if (i != j)
-
Cell(GetStringWidth(s[j..i]), h, s[j..i], 0, 0, '', fill, link);
-
end
-
end
-
1
alias_method :write, :Write
-
-
#
-
# Puts an image in the page. The upper-left corner must be given. The dimensions can be specified in different ways:<ul><li>explicit width and height (expressed in user unit)</li><li>one explicit dimension, the other being calculated automatically in order to keep the original proportions</li><li>no explicit dimension, in which case the image is put at 72 dpi</li></ul>
-
# Supported formats are JPEG and PNG.
-
# For JPEG, all flavors are allowed:<ul><li>gray scales</li><li>true colors (24 bits)</li><li>CMYK (32 bits)</li></ul>
-
# For PNG, are allowed:<ul><li>gray scales on at most 8 bits (256 levels)</li><li>indexed colors</li><li>true colors (24 bits)</li></ul>
-
# but are not supported:<ul><li>Interlacing</li><li>Alpha channel</li></ul>
-
# If a transparent color is defined, it will be taken into account (but will be only interpreted by Acrobat 4 and above).<br />
-
# The format can be specified explicitly or inferred from the file extension.<br />
-
# It is possible to put a link on the image.<br />
-
# Remark: if an image is used several times, only one copy will be embedded in the file.<br />
-
# @param string :file Name of the file containing the image.
-
# @param float :x Abscissa of the upper-left corner.
-
# @param float :y Ordinate of the upper-left corner.
-
# @param float :w Width of the image in the page. If not specified or equal to zero, it is automatically calculated.
-
# @param float :h Height of the image in the page. If not specified or equal to zero, it is automatically calculated.
-
# @param string :type Image format. Possible values are (case insensitive): JPG, JPEG, PNG. If not specified, the type is inferred from the file extension.
-
# @param mixed :link URL or identifier returned by AddLink().
-
# @since 1.1
-
# @see AddLink()
-
#
-
1
def Image(file, x, y, w=0, h=0, type='', link=nil)
-
#Put an image on the page
-
if (@images[file].nil?)
-
#First use of image, get info
-
if (type == '')
-
pos = File::basename(file).rindex('.');
-
if (pos.nil? or pos == 0)
-
Error('Image file has no extension and no type was specified: ' + file);
-
end
-
pos = file.rindex('.');
-
type = file[pos+1..-1];
-
end
-
type.downcase!
-
if (type == 'jpg' or type == 'jpeg')
-
info=parsejpg(file);
-
elsif (type == 'png')
-
info=parsepng(file);
-
elsif (type == 'gif')
-
tmpFile = imageToPNG(file);
-
info=parsepng(tmpFile.path);
-
tmpFile.delete
-
else
-
#Allow for additional formats
-
mtd='parse' + type;
-
if (!self.respond_to?(mtd))
-
Error('Unsupported image type: ' + type);
-
end
-
info=send(mtd, file);
-
end
-
info['i']=@images.length+1;
-
@images[file] = info;
-
else
-
info=@images[file];
-
end
-
#Automatic width and height calculation if needed
-
if ((w == 0) and (h == 0))
-
rescale_x = (@w - @r_margin - x) / (info['w'] / (@img_scale * @k))
-
rescale_x = 1 if rescale_x >= 1
-
if (y + info['h'] * rescale_x / (@img_scale * @k) > @page_break_trigger and !@in_footer and AcceptPageBreak())
-
#Automatic page break
-
if @pages[@page+1].nil?
-
ws = @ws;
-
if (ws > 0)
-
@ws = 0;
-
out('0 Tw');
-
end
-
AddPage(@cur_orientation);
-
if (ws > 0)
-
@ws = ws;
-
out(sprintf('%.3f Tw', ws * @k));
-
end
-
else
-
@page += 1;
-
end
-
y=@t_margin;
-
end
-
rescale_y = (@page_break_trigger - y) / (info['h'] / (@img_scale * @k))
-
rescale_y = 1 if rescale_y >= 1
-
rescale = rescale_y >= rescale_x ? rescale_x : rescale_y
-
-
#Put image at 72 dpi
-
# 2004-06-14 :: Nicola Asuni, scale factor where added
-
w = info['w'] * rescale / (@img_scale * @k);
-
h = info['h'] * rescale / (@img_scale * @k);
-
elsif (w == 0)
-
w = h * info['w'] / info['h'];
-
elsif (h == 0)
-
h = w * info['h'] / info['w'];
-
end
-
out(sprintf('q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q', w*@k, h*@k, x*@k, (@h-(y+h))*@k, info['i']));
-
if (link)
-
Link(x, y, w, h, link);
-
end
-
-
#2002-07-31 - Nicola Asuni
-
# set right-bottom corner coordinates
-
@img_rb_x = x + w;
-
@img_rb_y = y + h;
-
end
-
1
alias_method :image, :Image
-
-
#
-
# Performs a line break. The current abscissa goes back to the left margin and the ordinate increases by the amount passed in parameter.
-
# @param float :h The height of the break. By default, the value equals the height of the last printed cell.
-
# @since 1.0
-
# @see Cell()
-
#
-
1
def Ln(h='')
-
#Line feed; default value is last cell height
-
@x=@l_margin;
-
if (h.is_a?(String))
-
@y += @lasth;
-
else
-
@y += h;
-
end
-
-
k=@k;
-
if (@y > @page_break_trigger and !@in_footer and AcceptPageBreak())
-
#Automatic page break
-
if @pages[@page+1].nil?
-
x = @x;
-
ws = @ws;
-
if (ws > 0)
-
@ws = 0;
-
out('0 Tw');
-
end
-
AddPage(@cur_orientation);
-
@x = x;
-
if (ws > 0)
-
@ws = ws;
-
out(sprintf('%.3f Tw', ws * k));
-
end
-
else
-
@page += 1;
-
@y=@t_margin;
-
end
-
end
-
-
end
-
1
alias_method :ln, :Ln
-
-
#
-
# Returns the abscissa of the current position.
-
# @return float
-
# @since 1.2
-
# @see SetX(), GetY(), SetY()
-
#
-
1
def GetX()
-
#Get x position
-
return @x;
-
end
-
1
alias_method :get_x, :GetX
-
-
#
-
# Defines the abscissa of the current position. If the passed value is negative, it is relative to the right of the page.
-
# @param float :x The value of the abscissa.
-
# @since 1.2
-
# @see GetX(), GetY(), SetY(), SetXY()
-
#
-
1
def SetX(x)
-
#Set x position
-
if (x>=0)
-
@x = x;
-
else
-
@x=@w+x;
-
end
-
end
-
1
alias_method :set_x, :SetX
-
-
#
-
# Returns the ordinate of the current position.
-
# @return float
-
# @since 1.0
-
# @see SetY(), GetX(), SetX()
-
#
-
1
def GetY()
-
#Get y position
-
return @y;
-
end
-
1
alias_method :get_y, :GetY
-
-
#
-
# Moves the current abscissa back to the left margin and sets the ordinate. If the passed value is negative, it is relative to the bottom of the page.
-
# @param float :y The value of the ordinate.
-
# @since 1.0
-
# @see GetX(), GetY(), SetY(), SetXY()
-
#
-
1
def SetY(y)
-
#Set y position and reset x
-
@x=@l_margin;
-
if (y>=0)
-
@y = y;
-
else
-
@y=@h+y;
-
end
-
end
-
1
alias_method :set_y, :SetY
-
-
#
-
# Defines the abscissa and ordinate of the current position. If the passed values are negative, they are relative respectively to the right and bottom of the page.
-
# @param float :x The value of the abscissa
-
# @param float :y The value of the ordinate
-
# @since 1.2
-
# @see SetX(), SetY()
-
#
-
1
def SetXY(x, y)
-
#Set x and y positions
-
SetY(y);
-
SetX(x);
-
end
-
1
alias_method :set_xy, :SetXY
-
-
#
-
# Send the document to a given destination: string, local file or browser. In the last case, the plug-in may be used (if present) or a download ("Save as" dialog box) may be forced.<br />
-
# The method first calls Close() if necessary to terminate the document.
-
# @param string :name The name of the file. If not given, the document will be sent to the browser (destination I) with the name doc.pdf.
-
# @param string :dest Destination where to send the document. It can take one of the following values:<ul><li>I: send the file inline to the browser. The plug-in is used if available. The name given by name is used when one selects the "Save as" option on the link generating the PDF.</li><li>D: send to the browser and force a file download with the name given by name.</li><li>F: save to a local file with the name given by name.</li><li>S: return the document as a string. name is ignored.</li></ul>If the parameter is not specified but a name is given, destination is F. If no parameter is specified at all, destination is I.<br />
-
# @since 1.0
-
# @see Close()
-
#
-
1
def Output(name='', dest='')
-
#Output PDF to some destination
-
#Finish document if necessary
-
if (@state < 3)
-
Close();
-
end
-
#Normalize parameters
-
# Boolean no longer supported
-
# if (dest.is_a?(Boolean))
-
# dest = dest ? 'D' : 'F';
-
# end
-
dest = dest.upcase
-
if (dest=='')
-
if (name=='')
-
name='doc.pdf';
-
dest='I';
-
else
-
dest='F';
-
end
-
end
-
case (dest)
-
when 'I'
-
# This is PHP specific code
-
##Send to standard output
-
# if (ob_get_contents())
-
# Error('Some data has already been output, can\'t send PDF file');
-
# end
-
# if (php_sapi_name()!='cli')
-
# #We send to a browser
-
# header('Content-Type: application/pdf');
-
# if (headers_sent())
-
# Error('Some data has already been output to browser, can\'t send PDF file');
-
# end
-
# header('Content-Length: ' + @buffer.length);
-
# header('Content-disposition: inline; filename="' + name + '"');
-
# end
-
return @buffer;
-
-
when 'D'
-
# PHP specific
-
#Download file
-
# if (ob_get_contents())
-
# Error('Some data has already been output, can\'t send PDF file');
-
# end
-
# if (!_SERVER['HTTP_USER_AGENT'].nil? && SERVER['HTTP_USER_AGENT'].include?('MSIE'))
-
# header('Content-Type: application/force-download');
-
# else
-
# header('Content-Type: application/octet-stream');
-
# end
-
# if (headers_sent())
-
# Error('Some data has already been output to browser, can\'t send PDF file');
-
# end
-
# header('Content-Length: '+ @buffer.length);
-
# header('Content-disposition: attachment; filename="' + name + '"');
-
return @buffer;
-
-
when 'F'
-
open(name,'wb') do |f|
-
f.write(@buffer)
-
end
-
# PHP code
-
# #Save to local file
-
# f=open(name,'wb');
-
# if (!f)
-
# Error('Unable to create output file: ' + name);
-
# end
-
# fwrite(f,@buffer,@buffer.length);
-
# f.close
-
-
when 'S'
-
#Return as a string
-
return @buffer;
-
else
-
Error('Incorrect output destination: ' + dest);
-
-
end
-
return '';
-
end
-
1
alias_method :output, :Output
-
-
# Protected methods
-
-
#
-
# Check for locale-related bug
-
# @access protected
-
#
-
1
def dochecks()
-
#Check for locale-related bug
-
if (1.1==1)
-
Error('Don\'t alter the locale before including class file');
-
end
-
#Check for decimal separator
-
if (sprintf('%.1f',1.0)!='1.0')
-
setlocale(LC_NUMERIC,'C');
-
end
-
end
-
-
#
-
# Return fonts path
-
# @access protected
-
#
-
1
def getfontpath(file)
-
# Is it in the @@font_path?
-
if @@font_path
-
fpath = File.join @@font_path, file
-
if File.exists?(fpath)
-
return fpath
-
end
-
end
-
# Is it in this plugin's font folder?
-
fpath = File.join File.dirname(__FILE__), 'fonts', file
-
if File.exists?(fpath)
-
return fpath
-
end
-
# Could not find it.
-
nil
-
end
-
-
#
-
# Start document
-
# @access protected
-
#
-
1
def begindoc()
-
#Start document
-
@state=1;
-
out('%PDF-1.3');
-
end
-
-
#
-
# putpages
-
# @access protected
-
#
-
1
def putpages()
-
nb = @page;
-
if (@alias_nb_pages)
-
nbstr = UTF8ToUTF16BE(nb.to_s, false);
-
#Replace number of pages
-
1.upto(nb) do |n|
-
@pages[n].gsub!(@alias_nb_pages, nbstr)
-
end
-
end
-
if @def_orientation=='P'
-
w_pt=@fw_pt
-
h_pt=@fh_pt
-
else
-
w_pt=@fh_pt
-
h_pt=@fw_pt
-
end
-
filter=(@compress) ? '/Filter /FlateDecode ' : ''
-
1.upto(nb) do |n|
-
#Page
-
newobj
-
out('<</Type /Page')
-
out('/Parent 1 0 R')
-
unless @orientation_changes[n].nil?
-
out(sprintf('/MediaBox [0 0 %.2f %.2f]', h_pt, w_pt))
-
end
-
out('/Resources 2 0 R')
-
if @page_links[n]
-
#Links
-
annots='/Annots ['
-
@page_links[n].each do |pl|
-
rect=sprintf('%.2f %.2f %.2f %.2f', pl[0], pl[1], pl[0]+pl[2], pl[1]-pl[3]);
-
annots<<'<</Type /Annot /Subtype /Link /Rect [' + rect + '] /Border [0 0 0] ';
-
if (pl[4].is_a?(String))
-
annots<<'/A <</S /URI /URI (' + escape(pl[4]) + ')>>>>';
-
else
-
l=@links[pl[4]];
-
h=!@orientation_changes[l[0]].nil? ? w_pt : h_pt;
-
annots<<sprintf('/Dest [%d 0 R /XYZ 0 %.2f null]>>',1+2*l[0], h-l[1]*@k);
-
end
-
end
-
out(annots + ']');
-
end
-
out('/Contents ' + (@n+1).to_s + ' 0 R>>');
-
out('endobj');
-
#Page content
-
p=(@compress) ? gzcompress(@pages[n]) : @pages[n];
-
newobj();
-
out('<<' + filter + '/Length '+ p.length.to_s + '>>');
-
putstream(p);
-
out('endobj');
-
end
-
#Pages root
-
@offsets[1]=@buffer.length;
-
out('1 0 obj');
-
out('<</Type /Pages');
-
kids='/Kids [';
-
0.upto(nb) do |i|
-
kids<<(3+2*i).to_s + ' 0 R ';
-
end
-
out(kids + ']');
-
out('/Count ' + nb.to_s);
-
out(sprintf('/MediaBox [0 0 %.2f %.2f]', w_pt, h_pt));
-
out('>>');
-
out('endobj');
-
end
-
-
#
-
# Adds fonts
-
# putfonts
-
# @access protected
-
#
-
1
def putfonts()
-
nf=@n;
-
@diffs.each do |diff|
-
#Encodings
-
newobj();
-
out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [' + diff + ']>>');
-
out('endobj');
-
end
-
@font_files.each do |file, info|
-
#Font file embedding
-
newobj();
-
@font_files[file]['n']=@n;
-
font='';
-
open(getfontpath(file),'rb') do |f|
-
font = f.read();
-
end
-
compressed=(file[-2,2]=='.z');
-
if (!compressed && !info['length2'].nil?)
-
header=((font[0][0])==128);
-
if (header)
-
#Strip first binary header
-
font=font[6];
-
end
-
if header && (font[info['length1']][0] == 128)
-
#Strip second binary header
-
font=font[0..info['length1']] + font[info['length1']+6];
-
end
-
end
-
out('<</Length '+ font.length.to_s);
-
if (compressed)
-
out('/Filter /FlateDecode');
-
end
-
out('/Length1 ' + info['length1'].to_s);
-
if (!info['length2'].nil?)
-
out('/Length2 ' + info['length2'].to_s + ' /Length3 0');
-
end
-
out('>>');
-
open(getfontpath(file),'rb') do |f|
-
putstream(font)
-
end
-
out('endobj');
-
end
-
@fonts.each do |k, font|
-
#Font objects
-
@fonts[k]['n']=@n+1;
-
type = font['type'];
-
name = font['name'];
-
if (type=='core')
-
#Standard font
-
newobj();
-
out('<</Type /Font');
-
out('/BaseFont /' + name);
-
out('/Subtype /Type1');
-
if (name!='Symbol' && name!='ZapfDingbats')
-
out('/Encoding /WinAnsiEncoding');
-
end
-
out('>>');
-
out('endobj');
-
elsif type == 'Type0'
-
putType0(font)
-
elsif (type=='Type1' || type=='TrueType')
-
#Additional Type1 or TrueType font
-
newobj();
-
out('<</Type /Font');
-
out('/BaseFont /' + name);
-
out('/Subtype /' + type);
-
out('/FirstChar 32 /LastChar 255');
-
out('/Widths ' + (@n+1).to_s + ' 0 R');
-
out('/FontDescriptor ' + (@n+2).to_s + ' 0 R');
-
if (font['enc'])
-
if (!font['diff'].nil?)
-
out('/Encoding ' + (nf+font['diff']).to_s + ' 0 R');
-
else
-
out('/Encoding /WinAnsiEncoding');
-
end
-
end
-
out('>>');
-
out('endobj');
-
#Widths
-
newobj();
-
cw=font['cw']; # &
-
s='[';
-
32.upto(255) do |i|
-
s << cw[i.chr] + ' ';
-
end
-
out(s + ']');
-
out('endobj');
-
#Descriptor
-
newobj();
-
s='<</Type /FontDescriptor /FontName /' + name;
-
font['desc'].each do |k, v|
-
s<<' /' + k + ' ' + v;
-
end
-
file = font['file'];
-
if (file)
-
s<<' /FontFile' + (type=='Type1' ? '' : '2') + ' ' + @font_files[file]['n'] + ' 0 R';
-
end
-
out(s + '>>');
-
out('endobj');
-
else
-
#Allow for additional types
-
mtd='put' + type.downcase;
-
if (!self.respond_to?(mtd))
-
Error('Unsupported font type: ' + type)
-
else
-
self.send(mtd,font)
-
end
-
end
-
end
-
end
-
-
1
def putType0(font)
-
#Type0
-
newobj();
-
out('<</Type /Font')
-
out('/Subtype /Type0')
-
out('/BaseFont /'+font['name']+'-'+font['cMap'])
-
out('/Encoding /'+font['cMap'])
-
out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
-
out('>>')
-
out('endobj')
-
#CIDFont
-
newobj()
-
out('<</Type /Font')
-
out('/Subtype /CIDFontType0')
-
out('/BaseFont /'+font['name'])
-
out('/CIDSystemInfo <</Registry (Adobe) /Ordering ('+font['registry']['ordering']+') /Supplement '+font['registry']['supplement'].to_s+'>>')
-
out('/FontDescriptor '+(@n+1).to_s+' 0 R')
-
w='/W [1 ['
-
font['cw'].keys.sort.each {|key|
-
w+=font['cw'][key].to_s + " "
-
# ActionController::Base::logger.debug key.to_s
-
# ActionController::Base::logger.debug font['cw'][key].to_s
-
}
-
out(w+'] 231 325 500 631 [500] 326 389 500]')
-
out('>>')
-
out('endobj')
-
#Font descriptor
-
newobj()
-
out('<</Type /FontDescriptor')
-
out('/FontName /'+font['name'])
-
out('/Flags 6')
-
out('/FontBBox [0 -200 1000 900]')
-
out('/ItalicAngle 0')
-
out('/Ascent 800')
-
out('/Descent -200')
-
out('/CapHeight 800')
-
out('/StemV 60')
-
out('>>')
-
out('endobj')
-
end
-
-
#
-
# putimages
-
# @access protected
-
#
-
1
def putimages()
-
filter=(@compress) ? '/Filter /FlateDecode ' : '';
-
@images.each do |file, info| # was while(list(file, info)=each(@images))
-
newobj();
-
@images[file]['n']=@n;
-
out('<</Type /XObject');
-
out('/Subtype /Image');
-
out('/Width ' + info['w'].to_s);
-
out('/Height ' + info['h'].to_s);
-
if (info['cs']=='Indexed')
-
out('/ColorSpace [/Indexed /DeviceRGB ' + (info['pal'].length/3-1).to_s + ' ' + (@n+1).to_s + ' 0 R]');
-
else
-
out('/ColorSpace /' + info['cs']);
-
if (info['cs']=='DeviceCMYK')
-
out('/Decode [1 0 1 0 1 0 1 0]');
-
end
-
end
-
out('/BitsPerComponent ' + info['bpc'].to_s);
-
if (!info['f'].nil?)
-
out('/Filter /' + info['f']);
-
end
-
if (!info['parms'].nil?)
-
out(info['parms']);
-
end
-
if (!info['trns'].nil? and info['trns'].kind_of?(Array))
-
trns='';
-
0.upto(info['trns'].length) do |i|
-
trns << ("#{info['trns'][i]} " * 2);
-
end
-
out('/Mask [' + trns + ']');
-
end
-
out('/Length ' + info['data'].length.to_s + '>>');
-
putstream(info['data']);
-
@images[file]['data']=nil
-
out('endobj');
-
#Palette
-
if (info['cs']=='Indexed')
-
newobj();
-
pal=(@compress) ? gzcompress(info['pal']) : info['pal'];
-
out('<<' + filter + '/Length ' + pal.length.to_s + '>>');
-
putstream(pal);
-
out('endobj');
-
end
-
end
-
end
-
-
#
-
# putxobjectdict
-
# @access protected
-
#
-
1
def putxobjectdict()
-
@images.each_value do |image|
-
out('/I' + image['i'].to_s + ' ' + image['n'].to_s + ' 0 R');
-
end
-
end
-
-
#
-
# putresourcedict
-
# @access protected
-
#
-
1
def putresourcedict()
-
out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');
-
out('/Font <<');
-
@fonts.each_value do |font|
-
out('/F' + font['i'].to_s + ' ' + font['n'].to_s + ' 0 R');
-
end
-
out('>>');
-
out('/XObject <<');
-
putxobjectdict();
-
out('>>');
-
end
-
-
#
-
# putresources
-
# @access protected
-
#
-
1
def putresources()
-
putfonts();
-
putimages();
-
#Resource dictionary
-
@offsets[2]=@buffer.length;
-
out('2 0 obj');
-
out('<<');
-
putresourcedict();
-
out('>>');
-
out('endobj');
-
end
-
-
#
-
# putinfo
-
# @access protected
-
#
-
1
def putinfo()
-
out('/Producer ' + textstring(PDF_PRODUCER));
-
if (!@title.nil?)
-
out('/Title ' + textstring(@title));
-
end
-
if (!@subject.nil?)
-
out('/Subject ' + textstring(@subject));
-
end
-
if (!@author.nil?)
-
out('/Author ' + textstring(@author));
-
end
-
if (!@keywords.nil?)
-
out('/Keywords ' + textstring(@keywords));
-
end
-
if (!@creator.nil?)
-
out('/Creator ' + textstring(@creator));
-
end
-
out('/CreationDate ' + textstring('D:' + Time.now.strftime('%Y%m%d%H%M%S')));
-
end
-
-
#
-
# putcatalog
-
# @access protected
-
#
-
1
def putcatalog()
-
out('/Type /Catalog');
-
out('/Pages 1 0 R');
-
if (@zoom_mode=='fullpage')
-
out('/OpenAction [3 0 R /Fit]');
-
elsif (@zoom_mode=='fullwidth')
-
out('/OpenAction [3 0 R /FitH null]');
-
elsif (@zoom_mode=='real')
-
out('/OpenAction [3 0 R /XYZ null null 1]');
-
elsif (!@zoom_mode.is_a?(String))
-
out('/OpenAction [3 0 R /XYZ null null ' + (@zoom_mode/100) + ']');
-
end
-
if (@layout_mode=='single')
-
out('/PageLayout /SinglePage');
-
elsif (@layout_mode=='continuous')
-
out('/PageLayout /OneColumn');
-
elsif (@layout_mode=='two')
-
out('/PageLayout /TwoColumnLeft');
-
end
-
end
-
-
#
-
# puttrailer
-
# @access protected
-
#
-
1
def puttrailer()
-
out('/Size ' + (@n+1).to_s);
-
out('/Root ' + @n.to_s + ' 0 R');
-
out('/Info ' + (@n-1).to_s + ' 0 R');
-
end
-
-
#
-
# putheader
-
# @access protected
-
#
-
1
def putheader()
-
out('%PDF-' + @pdf_version);
-
end
-
-
#
-
# enddoc
-
# @access protected
-
#
-
1
def enddoc()
-
putheader();
-
putpages();
-
putresources();
-
#Info
-
newobj();
-
out('<<');
-
putinfo();
-
out('>>');
-
out('endobj');
-
#Catalog
-
newobj();
-
out('<<');
-
putcatalog();
-
out('>>');
-
out('endobj');
-
#Cross-ref
-
o=@buffer.length;
-
out('xref');
-
out('0 ' + (@n+1).to_s);
-
out('0000000000 65535 f ');
-
1.upto(@n) do |i|
-
out(sprintf('%010d 00000 n ',@offsets[i]));
-
end
-
#Trailer
-
out('trailer');
-
out('<<');
-
puttrailer();
-
out('>>');
-
out('startxref');
-
out(o);
-
out('%%EOF');
-
@state=3;
-
end
-
-
#
-
# beginpage
-
# @access protected
-
#
-
1
def beginpage(orientation)
-
@page += 1;
-
@pages[@page]='';
-
@state=2;
-
@x=@l_margin;
-
@y=@t_margin;
-
@font_family='';
-
#Page orientation
-
if (orientation.empty?)
-
orientation=@def_orientation;
-
else
-
orientation.upcase!
-
if (orientation!=@def_orientation)
-
@orientation_changes[@page]=true;
-
end
-
end
-
if (orientation!=@cur_orientation)
-
#Change orientation
-
if (orientation=='P')
-
@w_pt=@fw_pt;
-
@h_pt=@fh_pt;
-
@w=@fw;
-
@h=@fh;
-
else
-
@w_pt=@fh_pt;
-
@h_pt=@fw_pt;
-
@w=@fh;
-
@h=@fw;
-
end
-
@page_break_trigger=@h-@b_margin;
-
@cur_orientation = orientation;
-
end
-
end
-
-
#
-
# End of page contents
-
# @access protected
-
#
-
1
def endpage()
-
@state=1;
-
end
-
-
#
-
# Begin a new object
-
# @access protected
-
#
-
1
def newobj()
-
@n += 1;
-
@offsets[@n]=@buffer.length;
-
out(@n.to_s + ' 0 obj');
-
end
-
-
#
-
# Underline and Deleted text
-
# @access protected
-
#
-
1
def dolinetxt(x, y, txt)
-
up = @current_font['up'];
-
ut = @current_font['ut'];
-
w = GetStringWidth(txt) + @ws * txt.count(' ');
-
sprintf('%.2f %.2f %.2f %.2f re f', x * @k, (@h - (y - up / 1000.0 * @font_size)) * @k, w * @k, -ut / 1000.0 * @font_size_pt);
-
end
-
-
#
-
# Extract info from a JPEG file
-
# @access protected
-
#
-
1
def parsejpg(file)
-
a=getimagesize(file);
-
if (a.empty?)
-
Error('Missing or incorrect image file: ' + file);
-
end
-
if (!a[2].nil? and a[2]!='JPEG')
-
Error('Not a JPEG file: ' + file);
-
end
-
if (a['channels'].nil? or a['channels']==3)
-
colspace='DeviceRGB';
-
elsif (a['channels']==4)
-
colspace='DeviceCMYK';
-
else
-
colspace='DeviceGray';
-
end
-
bpc=!a['bits'].nil? ? a['bits'] : 8;
-
#Read whole file
-
data='';
-
-
open(file,'rb') do |f|
-
data<<f.read();
-
end
-
-
return {'w' => a[0],'h' => a[1],'cs' => colspace,'bpc' => bpc,'f'=>'DCTDecode','data' => data}
-
end
-
-
1
def imageToPNG(file)
-
return unless Object.const_defined?(:Magick)
-
-
img = Magick::ImageList.new(file)
-
img.format = 'PNG' # convert to PNG from gif
-
img.opacity = 0 # PNG alpha channel delete
-
-
#use a temporary file....
-
tmpFile = Tempfile.new(['', '_' + File::basename(file) + '.png'], @@k_path_cache);
-
tmpFile.binmode
-
tmpFile.print img.to_blob
-
tmpFile
-
ensure
-
tmpFile.close
-
end
-
-
#
-
# Extract info from a PNG file
-
# @access protected
-
#
-
1
def parsepng(file)
-
f=open(file,'rb');
-
#Check signature
-
if (f.read(8)!=137.chr + 'PNG' + 13.chr + 10.chr + 26.chr + 10.chr)
-
Error('Not a PNG file: ' + file);
-
end
-
#Read header chunk
-
f.read(4);
-
if (f.read(4)!='IHDR')
-
Error('Incorrect PNG file: ' + file);
-
end
-
w=freadint(f);
-
h=freadint(f);
-
bpc=f.read(1).unpack('C')[0];
-
if (bpc>8)
-
Error('16-bit depth not supported: ' + file);
-
end
-
ct=f.read(1).unpack('C')[0];
-
if (ct==0)
-
colspace='DeviceGray';
-
elsif (ct==2)
-
colspace='DeviceRGB';
-
elsif (ct==3)
-
colspace='Indexed';
-
else
-
Error('Alpha channel not supported: ' + file);
-
end
-
if (f.read(1).unpack('C')[0] != 0)
-
Error('Unknown compression method: ' + file);
-
end
-
if (f.read(1).unpack('C')[0] != 0)
-
Error('Unknown filter method: ' + file);
-
end
-
if (f.read(1).unpack('C')[0] != 0)
-
Error('Interlacing not supported: ' + file);
-
end
-
f.read(4);
-
parms='/DecodeParms <</Predictor 15 /Colors ' + (ct==2 ? 3 : 1).to_s + ' /BitsPerComponent ' + bpc.to_s + ' /Columns ' + w.to_s + '>>';
-
#Scan chunks looking for palette, transparency and image data
-
pal='';
-
trns='';
-
data='';
-
begin
-
n=freadint(f);
-
type=f.read(4);
-
if (type=='PLTE')
-
#Read palette
-
pal=f.read( n);
-
f.read(4);
-
elsif (type=='tRNS')
-
#Read transparency info
-
t=f.read( n);
-
if (ct==0)
-
trns = t[1].unpack('C')[0]
-
elsif (ct==2)
-
trns = t[[1].unpack('C')[0], t[3].unpack('C')[0], t[5].unpack('C')[0]]
-
else
-
pos=t.index(0.chr);
-
unless (pos.nil?)
-
trns = [pos]
-
end
-
end
-
f.read(4);
-
elsif (type=='IDAT')
-
#Read image data block
-
data<<f.read( n);
-
f.read(4);
-
elsif (type=='IEND')
-
break;
-
else
-
f.read( n+4);
-
end
-
end while(n)
-
if (colspace=='Indexed' and pal.empty?)
-
Error('Missing palette in ' + file);
-
end
-
return {'w' => w, 'h' => h, 'cs' => colspace, 'bpc' => bpc, 'f'=>'FlateDecode', 'parms' => parms, 'pal' => pal, 'trns' => trns, 'data' => data}
-
ensure
-
f.close
-
end
-
-
#
-
# Read a 4-byte integer from file
-
# @access protected
-
#
-
1
def freadint(f)
-
# Read a 4-byte integer from file
-
a = f.read(4).unpack('N')
-
return a[0]
-
end
-
-
#
-
# Format a text string
-
# @access protected
-
#
-
1
def textstring(s)
-
if (@is_unicode)
-
#Convert string to UTF-16BE
-
s = UTF8ToUTF16BE(s, true);
-
end
-
return '(' + escape(s) + ')';
-
end
-
-
#
-
# Format a text string
-
# @access protected
-
#
-
1
def escapetext(s)
-
if (@is_unicode)
-
#Convert string to UTF-16BE
-
s = UTF8ToUTF16BE(s, false);
-
end
-
return escape(s);
-
end
-
-
#
-
# Add \ before \, ( and )
-
# @access protected
-
#
-
1
def escape(s)
-
# Add \ before \, ( and )
-
s.gsub('\\','\\\\\\').gsub('(','\\(').gsub(')','\\)').gsub(13.chr, '\r')
-
end
-
-
#
-
#
-
# @access protected
-
#
-
1
def putstream(s)
-
out('stream');
-
out(s);
-
out('endstream');
-
end
-
-
#
-
# Add a line to the document
-
# @access protected
-
#
-
1
def out(s)
-
if (@state==2)
-
@pages[@page] << s.to_s + "\n";
-
else
-
@buffer << s.to_s + "\n";
-
end
-
end
-
-
#
-
# Adds unicode fonts.<br>
-
# Based on PDF Reference 1.3 (section 5)
-
# @access protected
-
# @author Nicola Asuni
-
# @since 1.52.0.TC005 (2005-01-05)
-
#
-
1
def puttruetypeunicode(font)
-
# Type0 Font
-
# A composite font composed of other fonts, organized hierarchically
-
newobj();
-
out('<</Type /Font');
-
out('/Subtype /Type0');
-
out('/BaseFont /' + font['name'] + '');
-
out('/Encoding /Identity-H'); #The horizontal identity mapping for 2-byte CIDs; may be used with CIDFonts using any Registry, Ordering, and Supplement values.
-
out('/DescendantFonts [' + (@n + 1).to_s + ' 0 R]');
-
out('/ToUnicode ' + (@n + 2).to_s + ' 0 R');
-
out('>>');
-
out('endobj');
-
-
# CIDFontType2
-
# A CIDFont whose glyph descriptions are based on TrueType font technology
-
newobj();
-
out('<</Type /Font');
-
out('/Subtype /CIDFontType2');
-
out('/BaseFont /' + font['name'] + '');
-
out('/CIDSystemInfo ' + (@n + 2).to_s + ' 0 R');
-
out('/FontDescriptor ' + (@n + 3).to_s + ' 0 R');
-
if (!font['desc']['MissingWidth'].nil?)
-
out('/DW ' + font['desc']['MissingWidth'].to_s + ''); # The default width for glyphs in the CIDFont MissingWidth
-
end
-
w = "";
-
font['cw'].each do |cid, width|
-
w << '' + cid.to_s + ' [' + width.to_s + '] '; # define a specific width for each individual CID
-
end
-
out('/W [' + w + ']'); # A description of the widths for the glyphs in the CIDFont
-
out('/CIDToGIDMap ' + (@n + 4).to_s + ' 0 R');
-
out('>>');
-
out('endobj');
-
-
# ToUnicode
-
# is a stream object that contains the definition of the CMap
-
# (PDF Reference 1.3 chap. 5.9)
-
newobj();
-
out('<</Length 383>>');
-
out('stream');
-
out('/CIDInit /ProcSet findresource begin');
-
out('12 dict begin');
-
out('begincmap');
-
out('/CIDSystemInfo');
-
out('<</Registry (Adobe)');
-
out('/Ordering (UCS)');
-
out('/Supplement 0');
-
out('>> def');
-
out('/CMapName /Adobe-Identity-UCS def');
-
out('/CMapType 2 def');
-
out('1 begincodespacerange');
-
out('<0000> <FFFF>');
-
out('endcodespacerange');
-
out('1 beginbfrange');
-
out('<0000> <FFFF> <0000>');
-
out('endbfrange');
-
out('endcmap');
-
out('CMapName currentdict /CMap defineresource pop');
-
out('end');
-
out('end');
-
out('endstream');
-
out('endobj');
-
-
# CIDSystemInfo dictionary
-
# A dictionary containing entries that define the character collection of the CIDFont.
-
newobj();
-
out('<</Registry (Adobe)'); # A string identifying an issuer of character collections
-
out('/Ordering (UCS)'); # A string that uniquely names a character collection issued by a specific registry
-
out('/Supplement 0'); # The supplement number of the character collection.
-
out('>>');
-
out('endobj');
-
-
# Font descriptor
-
# A font descriptor describing the CIDFont default metrics other than its glyph widths
-
newobj();
-
out('<</Type /FontDescriptor');
-
out('/FontName /' + font['name']);
-
font['desc'].each do |key, value|
-
out('/' + key.to_s + ' ' + value.to_s);
-
end
-
if (font['file'])
-
# A stream containing a TrueType font program
-
out('/FontFile2 ' + @font_files[font['file']]['n'].to_s + ' 0 R');
-
end
-
out('>>');
-
out('endobj');
-
-
# Embed CIDToGIDMap
-
# A specification of the mapping from CIDs to glyph indices
-
newobj();
-
ctgfile = getfontpath(font['ctg'])
-
if (!ctgfile)
-
Error('Font file not found: ' + ctgfile);
-
end
-
size = File.size(ctgfile);
-
out('<</Length ' + size.to_s + '');
-
if (ctgfile[-2,2] == '.z') # check file extension
-
# Decompresses data encoded using the public-domain
-
# zlib/deflate compression method, reproducing the
-
# original text or binary data#
-
out('/Filter /FlateDecode');
-
end
-
out('>>');
-
open(ctgfile, "rb") do |f|
-
putstream(f.read())
-
end
-
out('endobj');
-
end
-
-
#
-
# Converts UTF-8 strings to codepoints array.<br>
-
# Invalid byte sequences will be replaced with 0xFFFD (replacement character)<br>
-
# Based on: http://www.faqs.org/rfcs/rfc3629.html
-
# <pre>
-
# Char. number range | UTF-8 octet sequence
-
# (hexadecimal) | (binary)
-
# --------------------+-----------------------------------------------
-
# 0000 0000-0000 007F | 0xxxxxxx
-
# 0000 0080-0000 07FF | 110xxxxx 10xxxxxx
-
# 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
-
# 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
-
# ---------------------------------------------------------------------
-
#
-
# ABFN notation:
-
# ---------------------------------------------------------------------
-
# UTF8-octets =#( UTF8-char )
-
# UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
-
# UTF8-1 = %x00-7F
-
# UTF8-2 = %xC2-DF UTF8-tail
-
#
-
# UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
-
# %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
-
# UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
-
# %xF4 %x80-8F 2( UTF8-tail )
-
# UTF8-tail = %x80-BF
-
# ---------------------------------------------------------------------
-
# </pre>
-
# @param string :str string to process.
-
# @return array containing codepoints (UTF-8 characters values)
-
# @access protected
-
# @author Nicola Asuni
-
# @since 1.53.0.TC005 (2005-01-05)
-
#
-
1
def UTF8StringToArray(str)
-
if (!@is_unicode)
-
return str; # string is not in unicode
-
end
-
-
unicode = [] # array containing unicode values
-
bytes = [] # array containing single character byte sequences
-
numbytes = 1; # number of octetc needed to represent the UTF-8 character
-
-
str = str.to_s; # force :str to be a string
-
-
str.each_byte do |char|
-
if (bytes.length == 0) # get starting octect
-
if (char <= 0x7F)
-
unicode << char # use the character "as is" because is ASCII
-
numbytes = 1
-
elsif ((char >> 0x05) == 0x06) # 2 bytes character (0x06 = 110 BIN)
-
bytes << ((char - 0xC0) << 0x06)
-
numbytes = 2
-
elsif ((char >> 0x04) == 0x0E) # 3 bytes character (0x0E = 1110 BIN)
-
bytes << ((char - 0xE0) << 0x0C)
-
numbytes = 3
-
elsif ((char >> 0x03) == 0x1E) # 4 bytes character (0x1E = 11110 BIN)
-
bytes << ((char - 0xF0) << 0x12)
-
numbytes = 4
-
else
-
# use replacement character for other invalid sequences
-
unicode << 0xFFFD
-
bytes = []
-
numbytes = 1
-
end
-
elsif ((char >> 0x06) == 0x02) # bytes 2, 3 and 4 must start with 0x02 = 10 BIN
-
bytes << (char - 0x80)
-
if (bytes.length == numbytes)
-
# compose UTF-8 bytes to a single unicode value
-
char = bytes[0]
-
1.upto(numbytes-1) do |j|
-
char += (bytes[j] << ((numbytes - j - 1) * 0x06))
-
end
-
if (((char >= 0xD800) and (char <= 0xDFFF)) or (char >= 0x10FFFF))
-
# The definition of UTF-8 prohibits encoding character numbers between
-
# U+D800 and U+DFFF, which are reserved for use with the UTF-16
-
# encoding form (as surrogate pairs) and do not directly represent
-
# characters
-
unicode << 0xFFFD; # use replacement character
-
else
-
unicode << char; # add char to array
-
end
-
# reset data for next char
-
bytes = []
-
numbytes = 1;
-
end
-
else
-
# use replacement character for other invalid sequences
-
unicode << 0xFFFD;
-
bytes = []
-
numbytes = 1;
-
end
-
end
-
return unicode;
-
end
-
-
#
-
# Converts UTF-8 strings to UTF16-BE.<br>
-
# Based on: http://www.faqs.org/rfcs/rfc2781.html
-
# <pre>
-
# Encoding UTF-16:
-
#
-
# Encoding of a single character from an ISO 10646 character value to
-
# UTF-16 proceeds as follows. Let U be the character number, no greater
-
# than 0x10FFFF.
-
#
-
# 1) If U < 0x10000, encode U as a 16-bit unsigned integer and
-
# terminate.
-
#
-
# 2) Let U' = U - 0x10000. Because U is less than or equal to 0x10FFFF,
-
# U' must be less than or equal to 0xFFFFF. That is, U' can be
-
# represented in 20 bits.
-
#
-
# 3) Initialize two 16-bit unsigned integers, W1 and W2, to 0xD800 and
-
# 0xDC00, respectively. These integers each have 10 bits free to
-
# encode the character value, for a total of 20 bits.
-
#
-
# 4) Assign the 10 high-order bits of the 20-bit U' to the 10 low-order
-
# bits of W1 and the 10 low-order bits of U' to the 10 low-order
-
# bits of W2. Terminate.
-
#
-
# Graphically, steps 2 through 4 look like:
-
# U' = yyyyyyyyyyxxxxxxxxxx
-
# W1 = 110110yyyyyyyyyy
-
# W2 = 110111xxxxxxxxxx
-
# </pre>
-
# @param string :str string to process.
-
# @param boolean :setbom if true set the Byte Order Mark (BOM = 0xFEFF)
-
# @return string
-
# @access protected
-
# @author Nicola Asuni
-
# @since 1.53.0.TC005 (2005-01-05)
-
# @uses UTF8StringToArray
-
#
-
1
def UTF8ToUTF16BE(str, setbom=true)
-
if (!@is_unicode)
-
return str; # string is not in unicode
-
end
-
outstr = ""; # string to be returned
-
unicode = UTF8StringToArray(str); # array containing UTF-8 unicode values
-
numitems = unicode.length;
-
-
if (setbom)
-
outstr << "\xFE\xFF"; # Byte Order Mark (BOM)
-
end
-
unicode.each do |char|
-
if (char == 0xFFFD)
-
outstr << "\xFF\xFD"; # replacement character
-
elsif (char < 0x10000)
-
outstr << (char >> 0x08).chr;
-
outstr << (char & 0xFF).chr;
-
else
-
char -= 0x10000;
-
w1 = 0xD800 | (char >> 0x10);
-
w2 = 0xDC00 | (char & 0x3FF);
-
outstr << (w1 >> 0x08).chr;
-
outstr << (w1 & 0xFF).chr;
-
outstr << (w2 >> 0x08).chr;
-
outstr << (w2 & 0xFF).chr;
-
end
-
end
-
return outstr;
-
end
-
-
# ====================================================
-
-
#
-
# Set header font.
-
# @param array :font font
-
# @since 1.1
-
#
-
1
def SetHeaderFont(font)
-
@header_font = font;
-
end
-
1
alias_method :set_header_font, :SetHeaderFont
-
-
#
-
# Set footer font.
-
# @param array :font font
-
# @since 1.1
-
#
-
1
def SetFooterFont(font)
-
@footer_font = font;
-
end
-
1
alias_method :set_footer_font, :SetFooterFont
-
-
#
-
# Set language array.
-
# @param array :language
-
# @since 1.1
-
#
-
1
def SetLanguageArray(language)
-
@l = language;
-
end
-
1
alias_method :set_language_array, :SetLanguageArray
-
#
-
# Set document barcode.
-
# @param string :bc barcode
-
#
-
1
def SetBarcode(bc="")
-
@barcode = bc;
-
end
-
-
#
-
# Print Barcode.
-
# @param int :x x position in user units
-
# @param int :y y position in user units
-
# @param int :w width in user units
-
# @param int :h height position in user units
-
# @param string :type type of barcode (I25, C128A, C128B, C128C, C39)
-
# @param string :style barcode style
-
# @param string :font font for text
-
# @param int :xres x resolution
-
# @param string :code code to print
-
#
-
1
def writeBarcode(x, y, w, h, type, style, font, xres, code)
-
require(File.dirname(__FILE__) + "/barcode/barcode.rb");
-
require(File.dirname(__FILE__) + "/barcode/i25object.rb");
-
require(File.dirname(__FILE__) + "/barcode/c39object.rb");
-
require(File.dirname(__FILE__) + "/barcode/c128aobject.rb");
-
require(File.dirname(__FILE__) + "/barcode/c128bobject.rb");
-
require(File.dirname(__FILE__) + "/barcode/c128cobject.rb");
-
-
if (code.empty?)
-
return;
-
end
-
-
if (style.empty?)
-
style = BCS_ALIGN_LEFT;
-
style |= BCS_IMAGE_PNG;
-
style |= BCS_TRANSPARENT;
-
#:style |= BCS_BORDER;
-
#:style |= BCS_DRAW_TEXT;
-
#:style |= BCS_STRETCH_TEXT;
-
#:style |= BCS_REVERSE_COLOR;
-
end
-
if (font.empty?) then font = BCD_DEFAULT_FONT; end
-
if (xres.empty?) then xres = BCD_DEFAULT_XRES; end
-
-
scale_factor = 1.5 * xres * @k;
-
bc_w = (w * scale_factor).round #width in points
-
bc_h = (h * scale_factor).round #height in points
-
-
case (type.upcase)
-
when "I25"
-
obj = I25Object.new(bc_w, bc_h, style, code);
-
when "C128A"
-
obj = C128AObject.new(bc_w, bc_h, style, code);
-
when "C128B"
-
obj = C128BObject.new(bc_w, bc_h, style, code);
-
when "C128C"
-
obj = C128CObject.new(bc_w, bc_h, style, code);
-
when "C39"
-
obj = C39Object.new(bc_w, bc_h, style, code);
-
end
-
-
obj.SetFont(font);
-
obj.DrawObject(xres);
-
-
#use a temporary file....
-
tmpName = tempnam(@@k_path_cache,'img');
-
imagepng(obj.getImage(), tmpName);
-
Image(tmpName, x, y, w, h, 'png');
-
obj.DestroyObject();
-
obj = nil
-
unlink(tmpName);
-
end
-
-
#
-
# Returns the PDF data.
-
#
-
1
def GetPDFData()
-
if (@state < 3)
-
Close();
-
end
-
return @buffer;
-
end
-
-
# --- HTML PARSER FUNCTIONS ---
-
-
#
-
# Allows to preserve some HTML formatting.<br />
-
# Supports: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, ins, del, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small
-
# @param string :html text to display
-
# @param boolean :ln if true add a new line after text (default = true)
-
# @param int :fill Indicates if the background must be painted (1) or transparent (0). Default value: 0.
-
#
-
1
def writeHTML(html, ln=true, fill=0, h=0)
-
-
@lasth = h if h > 0
-
if (@lasth == 0)
-
#set row height
-
@lasth = @font_size * @@k_cell_height_ratio;
-
end
-
-
@href = nil
-
@style = "";
-
@t_cells = [[]];
-
@table_id = 0;
-
-
# pre calculate
-
html.split(/(<[^>]+>)/).each do |element|
-
if "<" == element[0,1]
-
#Tag
-
if (element[1, 1] == '/')
-
closedHTMLTagCalc(element[2..-2].downcase);
-
else
-
#Extract attributes
-
# get tag name
-
tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0}
-
tag = tag[0].to_s.downcase;
-
-
# get attributes
-
attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/)
-
attrs = {}
-
attr_array.each do |name, value|
-
attrs[name.downcase] = value;
-
end
-
openHTMLTagCalc(tag, attrs);
-
end
-
end
-
end
-
@table_id = 0;
-
-
html.split(/(<[A-Za-z!?\/][^>]*?>)/).each do |element|
-
if "<" == element[0,1]
-
#Tag
-
if (element[1, 1] == '/')
-
closedHTMLTagHandler(element[2..-2].downcase);
-
else
-
#Extract attributes
-
# get tag name
-
tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0}
-
tag = tag[0].to_s.downcase;
-
-
# get attributes
-
attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/)
-
attrs = {}
-
attr_array.each do |name, value|
-
attrs[name.downcase] = value;
-
end
-
openHTMLTagHandler(tag, attrs, fill);
-
end
-
-
else
-
#Text
-
if (@tdbegin)
-
element.gsub!(/[\t\r\n\f]/, "");
-
@tdtext << element.gsub(/ /, " ");
-
elsif (@href)
-
element.gsub!(/[\t\r\n\f]/, "");
-
addHtmlLink(@href, element, fill);
-
elsif (@pre_state == true and element.length > 0)
-
Write(@lasth, unhtmlentities(element), '', fill);
-
elsif (element.strip.length > 0)
-
element.gsub!(/[\t\r\n\f]/, "");
-
element.gsub!(/ /, " ");
-
Write(@lasth, unhtmlentities(element), '', fill);
-
end
-
end
-
end
-
-
if (ln)
-
Ln(@lasth);
-
end
-
end
-
1
alias_method :write_html, :writeHTML
-
-
#
-
# Prints a cell (rectangular area) with optional borders, background color and html text string. The upper-left corner of the cell corresponds to the current position. After the call, the current position moves to the right or to the next line.<br />
-
# If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting.
-
# @param float :w Cell width. If 0, the cell extends up to the right margin.
-
# @param float :h Cell minimum height. The cell extends automatically if needed.
-
# @param float :x upper-left corner X coordinate
-
# @param float :y upper-left corner Y coordinate
-
# @param string :html html text to print. Default value: empty string.
-
# @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:<ul><li>0: no border (default)</li><li>1: frame</li></ul>or a string containing some or all of the following characters (in any order):<ul><li>L: left</li><li>T: top</li><li>R: right</li><li>B: bottom</li></ul>
-
# @param int :ln Indicates where the current position should go after the call. Possible values are:<ul><li>0: to the right</li><li>1: to the beginning of the next line</li><li>2: below</li></ul>
-
# Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0.
-
# @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0.
-
# @see Cell()
-
#
-
1
def writeHTMLCell(w, h, x, y, html='', border=0, ln=1, fill=0)
-
-
if (@lasth == 0)
-
#set row height
-
@lasth = @font_size * @@k_cell_height_ratio;
-
end
-
-
if (x == 0)
-
x = GetX();
-
end
-
if (y == 0)
-
y = GetY();
-
end
-
-
# get current page number
-
pagenum = @page;
-
-
SetX(x);
-
SetY(y);
-
-
if (w == 0)
-
w = @fw - x - @r_margin;
-
end
-
-
b=0;
-
if (border)
-
if (border==1)
-
border='LTRB';
-
b='LRT';
-
b2='LR';
-
elsif border.is_a?(String)
-
b2='';
-
if (border.include?('L'))
-
b2<<'L';
-
end
-
if (border.include?('R'))
-
b2<<'R';
-
end
-
b=(border.include?('T')) ? b2 + 'T' : b2;
-
end
-
end
-
-
# store original margin values
-
l_margin = @l_margin;
-
r_margin = @r_margin;
-
-
# set new margin values
-
SetLeftMargin(x);
-
SetRightMargin(@fw - x - w);
-
-
# calculate remaining vertical space on page
-
restspace = GetPageHeight() - GetY() - GetBreakMargin();
-
-
writeHTML(html, true, fill); # write html text
-
SetX(x)
-
-
currentY = GetY();
-
@auto_page_break = false;
-
# check if a new page has been created
-
if (@page > pagenum)
-
# design a cell around the text on first page
-
currentpage = @page;
-
@page = pagenum;
-
SetY(GetPageHeight() - restspace - GetBreakMargin());
-
SetX(x)
-
Cell(w, restspace - 1, "", b, 0, 'L', 0);
-
b = b2;
-
@page += 1;
-
while @page < currentpage
-
SetY(@t_margin); # put cursor at the beginning of text
-
SetX(x)
-
Cell(w, @page_break_trigger - @t_margin, "", b, 0, 'L', 0);
-
@page += 1;
-
end
-
if (border.is_a?(String) and border.include?('B'))
-
b<<'B';
-
end
-
# design a cell around the text on last page
-
SetY(@t_margin); # put cursor at the beginning of text
-
SetX(x)
-
Cell(w, currentY - @t_margin, "", b, 0, 'L', 0);
-
else
-
SetY(y); # put cursor at the beginning of text
-
# design a cell around the text
-
SetX(x)
-
Cell(w, [h, (currentY - y)].max, "", border, 0, 'L', 0);
-
end
-
@auto_page_break = true;
-
-
# restore original margin values
-
SetLeftMargin(l_margin);
-
SetRightMargin(r_margin);
-
-
@lasth = h
-
-
# move cursor to specified position
-
if (ln == 0)
-
# go to the top-right of the cell
-
@x = x + w;
-
@y = y;
-
elsif (ln == 1)
-
# go to the beginning of the next line
-
@x = @l_margin;
-
@y = currentY;
-
elsif (ln == 2)
-
# go to the bottom-left of the cell (below)
-
@x = x;
-
@y = currentY;
-
end
-
end
-
1
alias_method :write_html_cell, :writeHTMLCell
-
-
#
-
# Check html table tag position.
-
#
-
# @param array :table potision array
-
# @param int :current tr tag id number
-
# @param int :current td tag id number
-
# @access private
-
# @return int : next td_id position.
-
# value 0 mean that can use position.
-
#
-
1
def checkTableBlockingCellPosition(table, tr_id, td_id )
-
0.upto(tr_id) do |j|
-
0.upto(@t_cells[table][j].size - 1) do |i|
-
if @t_cells[table][j][i]['i0'] <= td_id and td_id <= @t_cells[table][j][i]['i1']
-
if @t_cells[table][j][i]['j0'] <= tr_id and tr_id <= @t_cells[table][j][i]['j1']
-
return @t_cells[table][j][i]['i1'] - td_id + 1;
-
end
-
end
-
end
-
end
-
return 0;
-
end
-
-
#
-
# Calculate opening tags.
-
#
-
# html table cell array : @t_cells
-
#
-
# i0: table cell start position
-
# i1: table cell end position
-
# j0: table row start position
-
# j1: table row end position
-
#
-
# +------+
-
# |i0,j0 |
-
# | i1,j1|
-
# +------+
-
#
-
# example html:
-
# <table>
-
# <tr><td></td><td></td><td></td></tr>
-
# <tr><td colspan=2></td><td></td></tr>
-
# <tr><td rowspan=2></td><td></td><td></td></tr>
-
# <tr><td></td><td></td></tr>
-
# </table>
-
#
-
# i: 0 1 2
-
# j+----+----+----+
-
# :|0,0 |1,0 |2,0 |
-
# 0| 0,0| 1,0| 2,0|
-
# +----+----+----+
-
# |0,1 |2,1 |
-
# 1| 1,1| 2,1|
-
# +----+----+----+
-
# |0,2 |1,2 |2,2 |
-
# 2| | 1,2| 2,2|
-
# + +----+----+
-
# | |1,3 |2,3 |
-
# 3| 0,3| 1,3| 2,3|
-
# +----+----+----+
-
#
-
# html table cell array :
-
# [[[i0=>0,j0=>0,i1=>0,j1=>0],[i0=>1,j0=>0,i1=>1,j1=>0],[i0=>2,j0=>0,i1=>2,j1=>0]],
-
# [[i0=>0,j0=>1,i1=>1,j1=>1],[i0=>2,j0=>1,i1=>2,j1=>1]],
-
# [[i0=>0,j0=>2,i1=>0,j1=>3],[i0=>1,j0=>2,i1=>1,j1=>2],[i0=>2,j0=>2,i1=>2,j1=>2]]
-
# [[i0=>1,j0=>3,i1=>1,j1=>3],[i0=>2,j0=>3,i1=>2,j1=>3]]]
-
#
-
# @param string :tag tag name (in upcase)
-
# @param string :attr tag attribute (in upcase)
-
# @access private
-
#
-
1
def openHTMLTagCalc(tag, attrs)
-
#Opening tag
-
case (tag)
-
when 'table'
-
@max_table_columns[@table_id] = 0;
-
@t_columns = 0;
-
@tr_id = -1;
-
when 'tr'
-
if @max_table_columns[@table_id] < @t_columns
-
@max_table_columns[@table_id] = @t_columns;
-
end
-
@t_columns = 0;
-
@tr_id += 1;
-
@td_id = -1;
-
@t_cells[@table_id].push []
-
when 'td', 'th'
-
@td_id += 1;
-
if attrs['colspan'].nil? or attrs['colspan'] == ''
-
colspan = 1;
-
else
-
colspan = attrs['colspan'].to_i;
-
end
-
if attrs['rowspan'].nil? or attrs['rowspan'] == ''
-
rowspan = 1;
-
else
-
rowspan = attrs['rowspan'].to_i;
-
end
-
-
i = 0;
-
while true
-
next_i_distance = checkTableBlockingCellPosition(@table_id, @tr_id, @td_id + i);
-
if next_i_distance == 0
-
@t_cells[@table_id][@tr_id].push "i0"=>@td_id + i, "j0"=>@tr_id, "i1"=>(@td_id + i + colspan - 1), "j1"=>@tr_id + rowspan - 1
-
break;
-
end
-
i += next_i_distance;
-
end
-
-
@t_columns += colspan;
-
end
-
end
-
-
#
-
# Calculate closing tags.
-
# @param string :tag tag name (in upcase)
-
# @access private
-
#
-
1
def closedHTMLTagCalc(tag)
-
#Closing tag
-
case (tag)
-
when 'table'
-
if @max_table_columns[@table_id] < @t_columns
-
@max_table_columns[@table_id] = @t_columns;
-
end
-
@table_id += 1;
-
@t_cells.push []
-
end
-
end
-
-
#
-
# Convert to accessible file path
-
# @param string :attrname image file name
-
#
-
1
def getImageFilename( attrname )
-
nil
-
end
-
-
#
-
# Process opening tags.
-
# @param string :tag tag name (in upcase)
-
# @param string :attr tag attribute (in upcase)
-
# @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0.
-
# @access private
-
#
-
1
def openHTMLTagHandler(tag, attrs, fill=0)
-
#Opening tag
-
case (tag)
-
when 'pre'
-
@pre_state = true;
-
@l_margin += 5;
-
@r_margin += 5;
-
@x += 5;
-
-
when 'table'
-
Ln();
-
if @default_table_columns < @max_table_columns[@table_id]
-
@table_columns = @max_table_columns[@table_id];
-
else
-
@table_columns = @default_table_columns;
-
end
-
@l_margin += 5;
-
@r_margin += 5;
-
@x += 5;
-
-
if attrs['border'].nil? or attrs['border'] == ''
-
@tableborder = 0;
-
else
-
@tableborder = attrs['border'];
-
end
-
@tr_id = -1;
-
@max_td_page[0] = @page;
-
@max_td_y[0] = @y;
-
-
when 'tr', 'td', 'th'
-
if tag == 'th'
-
SetStyle('b', true);
-
@tdalign = "C";
-
end
-
if ((!attrs['width'].nil?) and (attrs['width'] != ''))
-
@tdwidth = (attrs['width'].to_i/4);
-
else
-
@tdwidth = ((@w - @l_margin - @r_margin) / @table_columns);
-
end
-
-
if tag == 'tr'
-
@tr_id += 1;
-
@td_id = -1;
-
else
-
@td_id += 1;
-
@x = @l_margin + @tdwidth * @t_cells[@table_id][@tr_id][@td_id]['i0'];
-
end
-
-
if attrs['colspan'].nil? or attrs['border'] == ''
-
@colspan = 1;
-
else
-
@colspan = attrs['colspan'].to_i;
-
end
-
@tdwidth *= @colspan;
-
if ((!attrs['height'].nil?) and (attrs['height'] != ''))
-
@tdheight=(attrs['height'].to_i / @k);
-
else
-
@tdheight = @lasth;
-
end
-
if ((!attrs['align'].nil?) and (attrs['align'] != ''))
-
case (attrs['align'])
-
when 'center'
-
@tdalign = "C";
-
when 'right'
-
@tdalign = "R";
-
when 'left'
-
@tdalign = "L";
-
end
-
end
-
if ((!attrs['bgcolor'].nil?) and (attrs['bgcolor'] != ''))
-
coul = convertColorHexToDec(attrs['bgcolor']);
-
SetFillColor(coul['R'], coul['G'], coul['B']);
-
@tdfill=1;
-
end
-
@tdbegin=true;
-
-
when 'hr'
-
margin = 1;
-
if ((!attrs['width'].nil?) and (attrs['width'] != ''))
-
hrWidth = attrs['width'];
-
else
-
hrWidth = @w - @l_margin - @r_margin - margin;
-
end
-
SetLineWidth(0.2);
-
Line(@x + margin, @y, @x + hrWidth, @y);
-
Ln();
-
-
when 'strong'
-
SetStyle('b', true);
-
-
when 'em'
-
SetStyle('i', true);
-
-
when 'ins'
-
SetStyle('u', true);
-
-
when 'del'
-
SetStyle('d', true);
-
-
when 'b', 'i', 'u'
-
SetStyle(tag, true);
-
-
when 'a'
-
@href = attrs['href'];
-
-
when 'img'
-
if (!attrs['src'].nil?)
-
# Don't generates image inside table tag
-
if (@tdbegin)
-
@tdtext << attrs['src'];
-
return
-
end
-
# Only generates image include a pdf if RMagick is avalaible
-
unless Object.const_defined?(:Magick)
-
Write(@lasth, attrs['src'], '', fill);
-
return
-
end
-
file = getImageFilename(attrs['src'])
-
if (file.nil?)
-
Write(@lasth, attrs['src'], '', fill);
-
return
-
end
-
-
if (attrs['width'].nil?)
-
attrs['width'] = 0;
-
end
-
if (attrs['height'].nil?)
-
attrs['height'] = 0;
-
end
-
-
begin
-
Image(file, GetX(),GetY(), pixelsToMillimeters(attrs['width']), pixelsToMillimeters(attrs['height']));
-
#SetX(@img_rb_x);
-
SetY(@img_rb_y);
-
rescue => err
-
logger.error "pdf: Image: error: #{err.message}"
-
Write(@lasth, attrs['src'], '', fill);
-
end
-
end
-
-
when 'ul', 'ol'
-
if @li_count == 0
-
Ln() if @prevquote_count == @quote_count; # insert Ln for keeping quote lines
-
@prevquote_count = @quote_count;
-
end
-
if @li_state == true
-
Ln();
-
@li_state = false;
-
end
-
if tag == 'ul'
-
@list_ordered[@li_count] = false;
-
else
-
@list_ordered[@li_count] = true;
-
end
-
@list_count[@li_count] = 0;
-
@li_count += 1
-
-
when 'li'
-
Ln() if @li_state == true
-
if (@list_ordered[@li_count - 1])
-
@list_count[@li_count - 1] += 1;
-
@li_spacer = " " * @li_count + (@list_count[@li_count - 1]).to_s + ". ";
-
else
-
#unordered list simbol
-
@li_spacer = " " * @li_count + "- ";
-
end
-
Write(@lasth, @spacer + @li_spacer, '', fill);
-
@li_state = true;
-
-
when 'blockquote'
-
if (@quote_count == 0)
-
SetStyle('i', true);
-
@l_margin += 5;
-
else
-
@l_margin += 5 / 2;
-
end
-
@x = @l_margin;
-
@quote_top[@quote_count] = @y;
-
@quote_page[@quote_count] = @page;
-
@quote_count += 1
-
when 'br'
-
if @tdbegin
-
@tdtext << "\n"
-
return
-
end
-
Ln();
-
-
if (@li_spacer.length > 0)
-
@x += GetStringWidth(@li_spacer);
-
end
-
-
when 'p'
-
Ln();
-
0.upto(@quote_count - 1) do |i|
-
if @quote_page[i] == @page;
-
if @quote_top[i] == @y - @lasth; # fix start line
-
@quote_top[i] = @y;
-
end
-
else
-
if @quote_page[i] == @page - 1;
-
@quote_page[i] = @page; # fix start line
-
@quote_top[i] = @t_margin;
-
end
-
end
-
end
-
-
when 'sup'
-
currentfont_size = @font_size;
-
@tempfontsize = @font_size_pt;
-
SetFontSize(@font_size_pt * @@k_small_ratio);
-
SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio)));
-
-
when 'sub'
-
currentfont_size = @font_size;
-
@tempfontsize = @font_size_pt;
-
SetFontSize(@font_size_pt * @@k_small_ratio);
-
SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio)));
-
-
when 'small'
-
currentfont_size = @font_size;
-
@tempfontsize = @font_size_pt;
-
SetFontSize(@font_size_pt * @@k_small_ratio);
-
SetXY(GetX(), GetY() + ((currentfont_size - @font_size)/3));
-
-
when 'font'
-
if (!attrs['color'].nil? and attrs['color']!='')
-
coul = convertColorHexToDec(attrs['color']);
-
SetTextColor(coul['R'], coul['G'], coul['B']);
-
@issetcolor=true;
-
end
-
if (!attrs['face'].nil? and @fontlist.include?(attrs['face'].downcase))
-
SetFont(attrs['face'].downcase);
-
@issetfont=true;
-
end
-
if (!attrs['size'].nil?)
-
headsize = attrs['size'].to_i;
-
else
-
headsize = 0;
-
end
-
currentfont_size = @font_size;
-
@tempfontsize = @font_size_pt;
-
SetFontSize(@font_size_pt + headsize);
-
@lasth = @font_size * @@k_cell_height_ratio;
-
-
when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
-
Ln();
-
headsize = (4 - tag[1,1].to_f) * 2
-
@tempfontsize = @font_size_pt;
-
SetFontSize(@font_size_pt + headsize);
-
SetStyle('b', true);
-
@lasth = @font_size * @@k_cell_height_ratio;
-
-
end
-
end
-
-
#
-
# Process closing tags.
-
# @param string :tag tag name (in upcase)
-
# @access private
-
#
-
1
def closedHTMLTagHandler(tag)
-
#Closing tag
-
case (tag)
-
when 'pre'
-
@pre_state = false;
-
@l_margin -= 5;
-
@r_margin -= 5;
-
@x = @l_margin;
-
Ln();
-
-
when 'td','th'
-
base_page = @page;
-
base_x = @x;
-
base_y = @y;
-
-
MultiCell(@tdwidth, @tdheight, unhtmlentities(@tdtext.strip), @tableborder, @tdalign, @tdfill, 1);
-
tr_end = @t_cells[@table_id][@tr_id][@td_id]['j1'] + 1;
-
if @max_td_page[tr_end].nil? or (@max_td_page[tr_end] < @page)
-
@max_td_page[tr_end] = @page
-
@max_td_y[tr_end] = @y
-
elsif (@max_td_page[tr_end] == @page)
-
@max_td_y[tr_end] = @y if @max_td_y[tr_end].nil? or (@max_td_y[tr_end] < @y)
-
end
-
-
@page = base_page;
-
@x = base_x + @tdwidth;
-
@y = base_y;
-
@tdtext = '';
-
@tdbegin = false;
-
@tdwidth = 0;
-
@tdheight = 0;
-
@tdalign = "L";
-
SetStyle('b', false);
-
@tdfill = 0;
-
SetFillColor(@prevfill_color[0], @prevfill_color[1], @prevfill_color[2]);
-
-
when 'tr'
-
@y = @max_td_y[@tr_id + 1];
-
@x = @l_margin;
-
@page = @max_td_page[@tr_id + 1];
-
-
when 'table'
-
# Write Table Line
-
width = (@w - @l_margin - @r_margin) / @table_columns;
-
0.upto(@t_cells[@table_id].size - 1) do |j|
-
0.upto(@t_cells[@table_id][j].size - 1) do |i|
-
@page = @max_td_page[j]
-
i0=@t_cells[@table_id][j][i]['i0'];
-
j0=@t_cells[@table_id][j][i]['j0'];
-
i1=@t_cells[@table_id][j][i]['i1'];
-
j1=@t_cells[@table_id][j][i]['j1'];
-
-
Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j0]) # top
-
if ( @page == @max_td_page[j1 + 1])
-
Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @max_td_y[j1+1]) # left
-
Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j1+1]) # right
-
else
-
Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @page_break_trigger) # left
-
Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @page_break_trigger) # right
-
@page += 1;
-
while @page < @max_td_page[j1 + 1]
-
Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @page_break_trigger) # left
-
Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @page_break_trigger) # right
-
@page += 1;
-
end
-
Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @max_td_y[j1+1]) # left
-
Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @max_td_y[j1+1]) # right
-
end
-
Line(@l_margin + width * i0, @max_td_y[j1+1], @l_margin + width * (i1+1), @max_td_y[j1+1]) # bottom
-
end
-
end
-
-
@l_margin -= 5;
-
@r_margin -= 5;
-
@tableborder=0;
-
@table_id += 1;
-
-
when 'strong'
-
SetStyle('b', false);
-
-
when 'em'
-
SetStyle('i', false);
-
-
when 'ins'
-
SetStyle('u', false);
-
-
when 'del'
-
SetStyle('d', false);
-
-
when 'b', 'i', 'u'
-
SetStyle(tag, false);
-
-
when 'a'
-
@href = nil;
-
-
when 'p'
-
Ln();
-
-
when 'sup'
-
currentfont_size = @font_size;
-
SetFontSize(@tempfontsize);
-
@tempfontsize = @font_size_pt;
-
SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio)));
-
-
when 'sub'
-
currentfont_size = @font_size;
-
SetFontSize(@tempfontsize);
-
@tempfontsize = @font_size_pt;
-
SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio)));
-
-
when 'small'
-
currentfont_size = @font_size;
-
SetFontSize(@tempfontsize);
-
@tempfontsize = @font_size_pt;
-
SetXY(GetX(), GetY() - ((@font_size - currentfont_size)/3));
-
-
when 'font'
-
if (@issetcolor == true)
-
SetTextColor(@prevtext_color[0], @prevtext_color[1], @prevtext_color[2]);
-
end
-
if (@issetfont)
-
@font_family = @prevfont_family;
-
@font_style = @prevfont_style;
-
SetFont(@font_family);
-
@issetfont = false;
-
end
-
currentfont_size = @font_size;
-
SetFontSize(@tempfontsize);
-
@tempfontsize = @font_size_pt;
-
#@text_color = @prevtext_color;
-
@lasth = @font_size * @@k_cell_height_ratio;
-
-
when 'blockquote'
-
@quote_count -= 1
-
if (@quote_page[@quote_count] == @page)
-
Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @y) # quoto line
-
else
-
cur_page = @page;
-
cur_y = @y;
-
@page = @quote_page[@quote_count];
-
if (@quote_top[@quote_count] < @page_break_trigger)
-
Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @page_break_trigger) # quoto line
-
end
-
@page += 1;
-
while @page < cur_page
-
Line(@l_margin - 1, @t_margin, @l_margin - 1, @page_break_trigger) # quoto line
-
@page += 1;
-
end
-
@y = cur_y;
-
Line(@l_margin - 1, @t_margin, @l_margin - 1, @y) # quoto line
-
end
-
if (@quote_count <= 0)
-
SetStyle('i', false);
-
@l_margin -= 5;
-
else
-
@l_margin -= 5 / 2;
-
end
-
@x = @l_margin;
-
Ln() if @quote_count == 0
-
-
when 'ul', 'ol'
-
@li_count -= 1
-
if @li_state == true
-
Ln();
-
@li_state = false;
-
end
-
-
when 'li'
-
@li_spacer = "";
-
if @li_state == true
-
Ln();
-
@li_state = false;
-
end
-
-
when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
-
SetFontSize(@tempfontsize);
-
@tempfontsize = @font_size_pt;
-
SetStyle('b', false);
-
Ln();
-
@lasth = @font_size * @@k_cell_height_ratio;
-
-
if tag == 'h1' or tag == 'h2' or tag == 'h3' or tag == 'h4'
-
margin = 1;
-
hrWidth = @w - @l_margin - @r_margin - margin;
-
if tag == 'h1' or tag == 'h2'
-
SetLineWidth(0.2);
-
else
-
SetLineWidth(0.1);
-
end
-
Line(@x + margin, @y, @x + hrWidth, @y);
-
end
-
end
-
end
-
-
#
-
# Sets font style.
-
# @param string :tag tag name (in lowercase)
-
# @param boolean :enable
-
# @access private
-
#
-
1
def SetStyle(tag, enable)
-
#Modify style and select corresponding font
-
['b', 'i', 'u', 'd'].each do |s|
-
if tag.downcase == s
-
if enable
-
@style << s if ! @style.include?(s)
-
else
-
@style = @style.gsub(s,'')
-
end
-
end
-
end
-
SetFont('', @style);
-
end
-
-
#
-
# Output anchor link.
-
# @param string :url link URL
-
# @param string :name link name
-
# @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0.
-
# @access public
-
#
-
1
def addHtmlLink(url, name, fill=0)
-
#Put a hyperlink
-
SetTextColor(0, 0, 255);
-
SetStyle('u', true);
-
Write(@lasth, name, url, fill);
-
SetStyle('u', false);
-
SetTextColor(0);
-
end
-
-
#
-
# Returns an associative array (keys: R,G,B) from
-
# a hex html code (e.g. #3FE5AA).
-
# @param string :color hexadecimal html color [#rrggbb]
-
# @return array
-
# @access private
-
#
-
1
def convertColorHexToDec(color = "#000000")
-
tbl_color = {}
-
tbl_color['R'] = color[1,2].hex.to_i;
-
tbl_color['G'] = color[3,2].hex.to_i;
-
tbl_color['B'] = color[5,2].hex.to_i;
-
return tbl_color;
-
end
-
-
#
-
# Converts pixels to millimeters in 72 dpi.
-
# @param int :px pixels
-
# @return float millimeters
-
# @access private
-
#
-
1
def pixelsToMillimeters(px)
-
return px.to_f * 25.4 / 72;
-
end
-
-
#
-
# Reverse function for htmlentities.
-
# Convert entities in UTF-8.
-
#
-
# @param :text_to_convert Text to convert.
-
# @return string converted
-
#
-
1
def unhtmlentities(string)
-
if @@decoder.nil?
-
CGI.unescapeHTML(string)
-
else
-
@@decoder.decode(string)
-
end
-
end
-
-
end # END OF CLASS
-
-
#TODO 2007-05-25 (EJM) Level=0 -
-
#Handle special IE contype request
-
# if (!_SERVER['HTTP_USER_AGENT'].nil? and (_SERVER['HTTP_USER_AGENT']=='contype'))
-
# header('Content-Type: application/pdf');
-
# exit;
-
# }
-
# vim:ts=4:sw=4:
-
# = RedCloth - Textile and Markdown Hybrid for Ruby
-
#
-
# Homepage:: http://whytheluckystiff.net/ruby/redcloth/
-
# Author:: why the lucky stiff (http://whytheluckystiff.net/)
-
# Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
-
# License:: BSD
-
#
-
# (see http://hobix.com/textile/ for a Textile Reference.)
-
#
-
# Based on (and also inspired by) both:
-
#
-
# PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
-
# Textism for PHP: http://www.textism.com/tools/textile/
-
#
-
#
-
-
# = RedCloth
-
#
-
# RedCloth is a Ruby library for converting Textile and/or Markdown
-
# into HTML. You can use either format, intermingled or separately.
-
# You can also extend RedCloth to honor your own custom text stylings.
-
#
-
# RedCloth users are encouraged to use Textile if they are generating
-
# HTML and to use Markdown if others will be viewing the plain text.
-
#
-
# == What is Textile?
-
#
-
# Textile is a simple formatting style for text
-
# documents, loosely based on some HTML conventions.
-
#
-
# == Sample Textile Text
-
#
-
# h2. This is a title
-
#
-
# h3. This is a subhead
-
#
-
# This is a bit of paragraph.
-
#
-
# bq. This is a blockquote.
-
#
-
# = Writing Textile
-
#
-
# A Textile document consists of paragraphs. Paragraphs
-
# can be specially formatted by adding a small instruction
-
# to the beginning of the paragraph.
-
#
-
# h[n]. Header of size [n].
-
# bq. Blockquote.
-
# # Numeric list.
-
# * Bulleted list.
-
#
-
# == Quick Phrase Modifiers
-
#
-
# Quick phrase modifiers are also included, to allow formatting
-
# of small portions of text within a paragraph.
-
#
-
# \_emphasis\_
-
# \_\_italicized\_\_
-
# \*strong\*
-
# \*\*bold\*\*
-
# ??citation??
-
# -deleted text-
-
# +inserted text+
-
# ^superscript^
-
# ~subscript~
-
# @code@
-
# %(classname)span%
-
#
-
# ==notextile== (leave text alone)
-
#
-
# == Links
-
#
-
# To make a hypertext link, put the link text in "quotation
-
# marks" followed immediately by a colon and the URL of the link.
-
#
-
# Optional: text in (parentheses) following the link text,
-
# but before the closing quotation mark, will become a Title
-
# attribute for the link, visible as a tool tip when a cursor is above it.
-
#
-
# Example:
-
#
-
# "This is a link (This is a title) ":http://www.textism.com
-
#
-
# Will become:
-
#
-
# <a href="http://www.textism.com" title="This is a title">This is a link</a>
-
#
-
# == Images
-
#
-
# To insert an image, put the URL for the image inside exclamation marks.
-
#
-
# Optional: text that immediately follows the URL in (parentheses) will
-
# be used as the Alt text for the image. Images on the web should always
-
# have descriptive Alt text for the benefit of readers using non-graphical
-
# browsers.
-
#
-
# Optional: place a colon followed by a URL immediately after the
-
# closing ! to make the image into a link.
-
#
-
# Example:
-
#
-
# !http://www.textism.com/common/textist.gif(Textist)!
-
#
-
# Will become:
-
#
-
# <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
-
#
-
# With a link:
-
#
-
# !/common/textist.gif(Textist)!:http://textism.com
-
#
-
# Will become:
-
#
-
# <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
-
#
-
# == Defining Acronyms
-
#
-
# HTML allows authors to define acronyms via the tag. The definition appears as a
-
# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
-
# this should be used at least once for each acronym in documents where they appear.
-
#
-
# To quickly define an acronym in Textile, place the full text in (parentheses)
-
# immediately following the acronym.
-
#
-
# Example:
-
#
-
# ACLU(American Civil Liberties Union)
-
#
-
# Will become:
-
#
-
# <acronym title="American Civil Liberties Union">ACLU</acronym>
-
#
-
# == Adding Tables
-
#
-
# In Textile, simple tables can be added by seperating each column by
-
# a pipe.
-
#
-
# |a|simple|table|row|
-
# |And|Another|table|row|
-
#
-
# Attributes are defined by style definitions in parentheses.
-
#
-
# table(border:1px solid black).
-
# (background:#ddd;color:red). |{}| | | |
-
#
-
# == Using RedCloth
-
#
-
# RedCloth is simply an extension of the String class, which can handle
-
# Textile formatting. Use it like a String and output HTML with its
-
# RedCloth#to_html method.
-
#
-
# doc = RedCloth.new "
-
#
-
# h2. Test document
-
#
-
# Just a simple test."
-
#
-
# puts doc.to_html
-
#
-
# By default, RedCloth uses both Textile and Markdown formatting, with
-
# Textile formatting taking precedence. If you want to turn off Markdown
-
# formatting, to boost speed and limit the processor:
-
#
-
# class RedCloth::Textile.new( str )
-
-
1
class RedCloth3 < String
-
-
1
VERSION = '3.0.4'
-
1
DEFAULT_RULES = [:textile, :markdown]
-
-
#
-
# Two accessor for setting security restrictions.
-
#
-
# This is a nice thing if you're using RedCloth for
-
# formatting in public places (e.g. Wikis) where you
-
# don't want users to abuse HTML for bad things.
-
#
-
# If +:filter_html+ is set, HTML which wasn't
-
# created by the Textile processor will be escaped.
-
#
-
# If +:filter_styles+ is set, it will also disable
-
# the style markup specifier. ('{color: red}')
-
#
-
1
attr_accessor :filter_html, :filter_styles
-
-
#
-
# Accessor for toggling hard breaks.
-
#
-
# If +:hard_breaks+ is set, single newlines will
-
# be converted to HTML break tags. This is the
-
# default behavior for traditional RedCloth.
-
#
-
1
attr_accessor :hard_breaks
-
-
# Accessor for toggling lite mode.
-
#
-
# In lite mode, block-level rules are ignored. This means
-
# that tables, paragraphs, lists, and such aren't available.
-
# Only the inline markup for bold, italics, entities and so on.
-
#
-
# r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
-
# r.to_html
-
# #=> "And then? She <strong>fell</strong>!"
-
#
-
1
attr_accessor :lite_mode
-
-
#
-
# Accessor for toggling span caps.
-
#
-
# Textile places `span' tags around capitalized
-
# words by default, but this wreaks havoc on Wikis.
-
# If +:no_span_caps+ is set, this will be
-
# suppressed.
-
#
-
1
attr_accessor :no_span_caps
-
-
#
-
# Establishes the markup predence. Available rules include:
-
#
-
# == Textile Rules
-
#
-
# The following textile rules can be set individually. Or add the complete
-
# set of rules with the single :textile rule, which supplies the rule set in
-
# the following precedence:
-
#
-
# refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
-
# block_textile_table:: Textile table block structures
-
# block_textile_lists:: Textile list structures
-
# block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
-
# inline_textile_image:: Textile inline images
-
# inline_textile_link:: Textile inline links
-
# inline_textile_span:: Textile inline spans
-
# glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
-
#
-
# == Markdown
-
#
-
# refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
-
# block_markdown_setext:: Markdown setext headers
-
# block_markdown_atx:: Markdown atx headers
-
# block_markdown_rule:: Markdown horizontal rules
-
# block_markdown_bq:: Markdown blockquotes
-
# block_markdown_lists:: Markdown lists
-
# inline_markdown_link:: Markdown links
-
1
attr_accessor :rules
-
-
# Returns a new RedCloth object, based on _string_ and
-
# enforcing all the included _restrictions_.
-
#
-
# r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
-
# r.to_html
-
# #=>"<h1>A <b>bold</b> man</h1>"
-
#
-
1
def initialize( string, restrictions = [] )
-
2085
restrictions.each { |r| method( "#{ r }=" ).call( true ) }
-
2085
super( string )
-
end
-
-
#
-
# Generates HTML from the Textile contents.
-
#
-
# r = RedCloth.new( "And then? She *fell*!" )
-
# r.to_html( true )
-
# #=>"And then? She <strong>fell</strong>!"
-
#
-
1
def to_html( *rules )
-
2085
rules = DEFAULT_RULES if rules.empty?
-
# make our working copy
-
2085
text = self.dup
-
-
2085
@urlrefs = {}
-
2085
@shelf = []
-
2085
textile_rules = [:block_textile_table, :block_textile_lists,
-
:block_textile_prefix, :inline_textile_image, :inline_textile_link,
-
:inline_textile_code, :inline_textile_span, :glyphs_textile]
-
2085
markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
-
:block_markdown_bq, :block_markdown_lists,
-
:inline_markdown_reflink, :inline_markdown_link]
-
2085
@rules = rules.collect do |rule|
-
8340
case rule
-
when :markdown
-
markdown_rules
-
when :textile
-
2085
textile_rules
-
else
-
6255
rule
-
end
-
end.flatten
-
-
# standard clean up
-
2085
incoming_entities text
-
2085
clean_white_space text
-
-
# start processor
-
2085
@pre_list = []
-
2085
rip_offtags text
-
2085
no_textile text
-
2085
escape_html_tags text
-
# need to do this before #hard_break and #blocks
-
2085
block_textile_quotes text unless @lite_mode
-
2085
hard_break text
-
2085
unless @lite_mode
-
2085
refs text
-
2085
blocks text
-
end
-
2085
inline text
-
2085
smooth_offtags text
-
-
2085
retrieve text
-
-
2085
text.gsub!( /<\/?notextile>/, '' )
-
2085
text.gsub!( /x%x%/, '&' )
-
2085
clean_html text if filter_html
-
2085
text.strip!
-
2085
text
-
-
end
-
-
#######
-
1
private
-
#######
-
#
-
# Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
-
# (from PyTextile)
-
#
-
1
TEXTILE_TAGS =
-
-
[[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
-
[134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
-
[140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
-
[147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
-
[153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
-
-
collect! do |a, b|
-
32
[a.chr, ( b.zero? and "" or "&#{ b };" )]
-
end
-
-
#
-
# Regular expressions to convert to HTML.
-
#
-
1
A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
-
1
A_VLGN = /[\-^~]/
-
1
C_CLAS = '(?:\([^")]+\))'
-
1
C_LNGE = '(?:\[[^"\[\]]+\])'
-
1
C_STYL = '(?:\{[^"}]+\})'
-
1
S_CSPN = '(?:\\\\\d+)'
-
1
S_RSPN = '(?:/\d+)'
-
1
A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
-
1
S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
-
1
C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
-
# PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
-
1
PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
-
1
PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
-
1
PUNCT_Q = Regexp::quote( '*-_+^~%' )
-
1
HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
-
-
# Text markup tags, don't conflict with block tags
-
1
SIMPLE_HTML_TAGS = [
-
'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
-
'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
-
'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
-
]
-
-
1
QTAGS = [
-
['**', 'b', :limit],
-
['*', 'strong', :limit],
-
['??', 'cite', :limit],
-
['-', 'del', :limit],
-
['__', 'i', :limit],
-
['_', 'em', :limit],
-
['%', 'span', :limit],
-
['+', 'ins', :limit],
-
['^', 'sup', :limit],
-
['~', 'sub', :limit]
-
]
-
11
QTAGS_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|')
-
-
1
QTAGS.collect! do |rc, ht, rtype|
-
10
rcq = Regexp::quote rc
-
10
re =
-
case rtype
-
when :limit
-
/(^|[>\s\(]) # sta
-
(?!\-\-)
-
10
(#{QTAGS_JOIN}|) # oqs
-
(#{rcq}) # qtag
-
(\w|[^\s].*?[^\s]) # content
-
(?!\-\-)
-
#{rcq}
-
(#{QTAGS_JOIN}|) # oqa
-
(?=[[:punct:]]|<|\s|\)|$)/x
-
else
-
/(#{rcq})
-
(#{C})
-
(?::(\S+))?
-
(\w|[^\s\-].*?[^\s\-])
-
#{rcq}/xm
-
end
-
10
[rc, ht, re, rtype]
-
end
-
-
# Elements to handle
-
1
GLYPHS = [
-
# [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing
-
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1’' ], # single closing
-
# [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '’' ], # single closing
-
# [ /\'/, '‘' ], # single opening
-
# [ /</, '<' ], # less-than
-
# [ />/, '>' ], # greater-than
-
# [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing
-
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1”' ], # double closing
-
# [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '”' ], # double closing
-
# [ /"/, '“' ], # double opening
-
# [ /\b( )?\.{3}/, '\1…' ], # ellipsis
-
# [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
-
# [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
-
# [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash
-
# [ /\s->\s/, ' → ' ], # right arrow
-
# [ /\s-\s/, ' – ' ], # en dash
-
# [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign
-
# [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark
-
# [ /\b ?[(\[]R[\])]/i, '®' ], # registered
-
# [ /\b ?[(\[]C[\])]/i, '©' ] # copyright
-
]
-
-
1
H_ALGN_VALS = {
-
'<' => 'left',
-
'=' => 'center',
-
'>' => 'right',
-
'<>' => 'justify'
-
}
-
-
1
V_ALGN_VALS = {
-
'^' => 'top',
-
'-' => 'middle',
-
'~' => 'bottom'
-
}
-
-
#
-
# Flexible HTML escaping
-
#
-
1
def htmlesc( str, mode=:Quotes )
-
if str
-
str.gsub!( '&', '&' )
-
str.gsub!( '"', '"' ) if mode != :NoQuotes
-
str.gsub!( "'", ''' ) if mode == :Quotes
-
str.gsub!( '<', '<')
-
str.gsub!( '>', '>')
-
end
-
str
-
end
-
-
# Search and replace for Textile glyphs (quotes, dashes, other symbols)
-
1
def pgl( text )
-
#GLYPHS.each do |re, resub, tog|
-
# next if tog and method( tog ).call
-
# text.gsub! re, resub
-
#end
-
7352
text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m|
-
"<acronym title=\"#{htmlesc $2}\">#{$1}</acronym>"
-
end
-
end
-
-
# Parses Textile attribute lists and builds an HTML attribute string
-
1
def pba( text_in, element = "" )
-
-
3
return '' unless text_in
-
-
3
style = []
-
3
text = text_in.dup
-
3
if element == 'td'
-
colspan = $1 if text =~ /\\(\d+)/
-
rowspan = $1 if text =~ /\/(\d+)/
-
style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
-
end
-
-
3
if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles
-
sanitized = sanitize_styles($1)
-
style << "#{ sanitized };" unless sanitized.blank?
-
end
-
-
lang = $1 if
-
3
text.sub!( /\[([^)]+?)\]/, '' )
-
-
cls = $1 if
-
3
text.sub!( /\(([^()]+?)\)/, '' )
-
-
style << "padding-left:#{ $1.length }em;" if
-
3
text.sub!( /([(]+)/, '' )
-
-
3
style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
-
-
3
style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
-
-
3
cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
-
-
3
atts = ''
-
3
atts << " style=\"#{ style.join }\"" unless style.empty?
-
3
atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
-
3
atts << " lang=\"#{ lang }\"" if lang
-
3
atts << " id=\"#{ id }\"" if id
-
3
atts << " colspan=\"#{ colspan }\"" if colspan
-
3
atts << " rowspan=\"#{ rowspan }\"" if rowspan
-
-
3
atts
-
end
-
-
1
STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
-
-
1
def sanitize_styles(str)
-
styles = str.split(";").map(&:strip)
-
styles.reject! do |style|
-
!style.match(STYLES_RE)
-
end
-
styles.join(";")
-
end
-
-
1
TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
-
-
# Parses a Textile table block, building HTML from the result.
-
1
def block_textile_table( text )
-
1085
text.gsub!( TABLE_RE ) do |matches|
-
-
tatts, fullrow = $~[1..2]
-
tatts = pba( tatts, 'table' )
-
tatts = shelve( tatts ) if tatts
-
rows = []
-
-
fullrow.each_line do |row|
-
ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
-
cells = []
-
row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
-
next if cell == '|'
-
ctyp = 'd'
-
ctyp = 'h' if cell =~ /^_/
-
-
catts = ''
-
catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
-
-
catts = shelve( catts ) if catts
-
cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
-
end
-
ratts = shelve( ratts ) if ratts
-
rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
-
end
-
"\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
-
end
-
end
-
-
1
LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
-
1
LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
-
-
# Parses Textile lists and generates HTML
-
1
def block_textile_lists( text )
-
1085
text.gsub!( LISTS_RE ) do |match|
-
lines = match.split( /\n/ )
-
last_line = -1
-
depth = []
-
lines.each_with_index do |line, line_id|
-
if line =~ LISTS_CONTENT_RE
-
tl,atts,content = $~[1..3]
-
if depth.last
-
if depth.last.length > tl.length
-
(depth.length - 1).downto(0) do |i|
-
break if depth[i].length == tl.length
-
lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
-
depth.pop
-
end
-
end
-
if depth.last and depth.last.length == tl.length
-
lines[line_id - 1] << '</li>'
-
end
-
end
-
unless depth.last == tl
-
depth << tl
-
atts = pba( atts )
-
atts = shelve( atts ) if atts
-
lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
-
else
-
lines[line_id] = "\t\t<li>#{ content }"
-
end
-
last_line = line_id
-
-
else
-
last_line = line_id
-
end
-
if line_id - last_line > 1 or line_id == lines.length - 1
-
while v = depth.pop
-
lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
-
end
-
end
-
end
-
lines.join( "\n" )
-
end
-
end
-
-
1
QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
-
1
QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
-
-
1
def block_textile_quotes( text )
-
2085
text.gsub!( QUOTES_RE ) do |match|
-
lines = match.split( /\n/ )
-
quotes = ''
-
indent = 0
-
lines.each do |line|
-
line =~ QUOTES_CONTENT_RE
-
bq,content = $1, $2
-
l = bq.count('>')
-
if l != indent
-
quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
-
indent = l
-
end
-
quotes << (content + "\n")
-
end
-
quotes << ("\n" + '</blockquote>' * indent + "\n\n")
-
quotes
-
end
-
end
-
-
1
CODE_RE = /(\W)
-
@
-
(?:\|(\w+?)\|)?
-
(.+?)
-
@
-
(?=\W)/x
-
-
1
def inline_textile_code( text )
-
2085
text.gsub!( CODE_RE ) do |m|
-
before,lang,code,after = $~[1..4]
-
lang = " lang=\"#{ lang }\"" if lang
-
rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }", false )
-
end
-
end
-
-
1
def lT( text )
-
text =~ /\#$/ ? 'o' : 'u'
-
end
-
-
1
def hard_break( text )
-
text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
-
end
-
-
1
BLOCKS_GROUP_RE = /\n{2,}(?! )/m
-
-
1
def blocks( text, deep_code = false )
-
2085
text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
-
1085
plain = blk !~ /\A[#*> ]/
-
-
# skip blocks that are complex HTML
-
1085
if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
-
blk
-
else
-
# search for indentation levels
-
1085
blk.strip!
-
1085
if blk.empty?
-
blk
-
else
-
1085
code_blk = nil
-
1085
blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
-
flush_left iblk
-
blocks iblk, plain
-
iblk.gsub( /^(\S)/, "\t\\1" )
-
if plain
-
code_blk = iblk; ""
-
else
-
iblk
-
end
-
end
-
-
1085
block_applied = 0
-
1085
@rules.each do |rule_name|
-
11935
block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
-
end
-
1085
if block_applied.zero?
-
1082
if deep_code
-
blk = "\t<pre><code>#{ blk }</code></pre>"
-
else
-
1082
blk = "\t<p>#{ blk }</p>"
-
end
-
end
-
# hard_break blk
-
1085
blk + "\n#{ code_blk }"
-
end
-
end
-
-
end.join( "\n\n" ) )
-
end
-
-
1
def textile_bq( tag, atts, cite, content )
-
cite, cite_title = check_refs( cite )
-
cite = " cite=\"#{ cite }\"" if cite
-
atts = shelve( atts ) if atts
-
"\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
-
end
-
-
1
def textile_p( tag, atts, cite, content )
-
3
atts = shelve( atts ) if atts
-
3
"\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
-
end
-
-
1
alias textile_h1 textile_p
-
1
alias textile_h2 textile_p
-
1
alias textile_h3 textile_p
-
1
alias textile_h4 textile_p
-
1
alias textile_h5 textile_p
-
1
alias textile_h6 textile_p
-
-
1
def textile_fn_( tag, num, atts, cite, content )
-
atts << " id=\"fn#{ num }\" class=\"footnote\""
-
content = "<sup>#{ num }</sup> #{ content }"
-
atts = shelve( atts ) if atts
-
"\t<p#{ atts }>#{ content }</p>"
-
end
-
-
1
BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
-
-
1
def block_textile_prefix( text )
-
1085
if text =~ BLOCK_RE
-
3
tag,tagpre,num,atts,cite,content = $~[1..6]
-
3
atts = pba( atts )
-
-
# pass to prefix handler
-
3
replacement = nil
-
3
if respond_to? "textile_#{ tag }", true
-
3
replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content )
-
elsif respond_to? "textile_#{ tagpre }_", true
-
replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content )
-
end
-
6
text.gsub!( $& ) { replacement } if replacement
-
end
-
end
-
-
1
SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
-
1
def block_markdown_setext( text )
-
if text =~ SETEXT_RE
-
tag = if $2 == "="; "h1"; else; "h2"; end
-
blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
-
blocks cont
-
text.replace( blk + cont )
-
end
-
end
-
-
1
ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
-
[ ]*
-
(.+?) # $2 = Header text
-
[ ]*
-
\#* # optional closing #'s (not counted)
-
$/x
-
1
def block_markdown_atx( text )
-
if text =~ ATX_RE
-
tag = "h#{ $1.length }"
-
blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
-
blocks cont
-
text.replace( blk + cont )
-
end
-
end
-
-
1
MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
-
-
1
def block_markdown_bq( text )
-
text.gsub!( MARKDOWN_BQ_RE ) do |blk|
-
blk.gsub!( /^ *> ?/, '' )
-
flush_left blk
-
blocks blk
-
blk.gsub!( /^(\S)/, "\t\\1" )
-
"<blockquote>\n#{ blk }\n</blockquote>\n\n"
-
end
-
end
-
-
1
MARKDOWN_RULE_RE = /^(#{
-
3
['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
-
})$/
-
-
1
def block_markdown_rule( text )
-
1085
text.gsub!( MARKDOWN_RULE_RE ) do |blk|
-
"<hr />"
-
end
-
end
-
-
# XXX TODO XXX
-
1
def block_markdown_lists( text )
-
end
-
-
1
def inline_textile_span( text )
-
2085
QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
-
20850
text.gsub!( qtag_re ) do |m|
-
-
case rtype
-
when :limit
-
sta,oqs,qtag,content,oqa = $~[1..6]
-
atts = nil
-
if content =~ /^(#{C})(.+)$/
-
atts, content = $~[1..2]
-
end
-
else
-
qtag,atts,cite,content = $~[1..4]
-
sta = ''
-
end
-
atts = pba( atts )
-
atts = shelve( atts ) if atts
-
-
"#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }</#{ ht }>#{ oqa }"
-
-
end
-
end
-
end
-
-
1
LINK_RE = /
-
(
-
([\s\[{(]|[#{PUNCT}])? # $pre
-
" # start
-
(#{C}) # $atts
-
([^"\n]+?) # $text
-
\s?
-
(?:\(([^)]+?)\)(?="))? # $title
-
":
-
( # $url
-
(\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
-
[[:alnum:]_\/]\S+?
-
)
-
(\/)? # $slash
-
([^[:alnum:]_\=\/;\(\)]*?) # $post
-
)
-
(?=<|\s|$)
-
/x
-
#"
-
1
def inline_textile_link( text )
-
2085
text.gsub!( LINK_RE ) do |m|
-
all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
-
if text.include?('<br />')
-
all
-
else
-
url, url_title = check_refs( url )
-
title ||= url_title
-
-
# Idea below : an URL with unbalanced parethesis and
-
# ending by ')' is put into external parenthesis
-
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
-
url=url[0..-2] # discard closing parenth from url
-
post = ")"+post # add closing parenth to post
-
end
-
atts = pba( atts )
-
atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
-
atts << " title=\"#{ htmlesc title }\"" if title
-
atts = shelve( atts ) if atts
-
-
external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
-
-
"#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
-
end
-
end
-
end
-
-
1
MARKDOWN_REFLINK_RE = /
-
\[([^\[\]]+)\] # $text
-
[ ]? # opt. space
-
(?:\n[ ]*)? # one optional newline followed by spaces
-
\[(.*?)\] # $id
-
/x
-
-
1
def inline_markdown_reflink( text )
-
text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
-
text, id = $~[1..2]
-
-
if id.empty?
-
url, title = check_refs( text )
-
else
-
url, title = check_refs( id )
-
end
-
-
atts = " href=\"#{ url }\""
-
atts << " title=\"#{ title }\"" if title
-
atts = shelve( atts )
-
-
"<a#{ atts }>#{ text }</a>"
-
end
-
end
-
-
1
MARKDOWN_LINK_RE = /
-
\[([^\[\]]+)\] # $text
-
\( # open paren
-
[ \t]* # opt space
-
<?(.+?)>? # $href
-
[ \t]* # opt space
-
(?: # whole title
-
(['"]) # $quote
-
(.*?) # $title
-
\3 # matching quote
-
)? # title is optional
-
\)
-
/x
-
-
1
def inline_markdown_link( text )
-
text.gsub!( MARKDOWN_LINK_RE ) do |m|
-
text, url, quote, title = $~[1..4]
-
-
atts = " href=\"#{ url }\""
-
atts << " title=\"#{ title }\"" if title
-
atts = shelve( atts )
-
-
"<a#{ atts }>#{ text }</a>"
-
end
-
end
-
-
1
TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
-
1
MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
-
-
1
def refs( text )
-
2085
@rules.each do |rule_name|
-
22935
method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
-
end
-
end
-
-
1
def refs_textile( text )
-
text.gsub!( TEXTILE_REFS_RE ) do |m|
-
flag, url = $~[2..3]
-
@urlrefs[flag.downcase] = [url, nil]
-
nil
-
end
-
end
-
-
1
def refs_markdown( text )
-
text.gsub!( MARKDOWN_REFS_RE ) do |m|
-
flag, url = $~[2..3]
-
title = $~[6]
-
@urlrefs[flag.downcase] = [url, title]
-
nil
-
end
-
end
-
-
1
def check_refs( text )
-
ret = @urlrefs[text.downcase] if text
-
ret || [text, nil]
-
end
-
-
1
IMAGE_RE = /
-
(>|\s|^) # start of line?
-
\! # opening
-
(\<|\=|\>)? # optional alignment atts
-
(#{C}) # optional style,class atts
-
(?:\. )? # optional dot-space
-
([^\s(!]+?) # presume this is the src
-
\s? # optional space
-
(?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
-
\! # closing
-
(?::#{ HYPERLINK })? # optional href
-
/x
-
-
1
def inline_textile_image( text )
-
2085
text.gsub!( IMAGE_RE ) do |m|
-
stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
-
htmlesc title
-
atts = pba( atts )
-
atts = " src=\"#{ htmlesc url.dup }\"#{ atts }"
-
atts << " title=\"#{ title }\"" if title
-
atts << " alt=\"#{ title }\""
-
# size = @getimagesize($url);
-
# if($size) $atts.= " $size[3]";
-
-
href, alt_title = check_refs( href ) if href
-
url, url_title = check_refs( url )
-
-
out = ''
-
out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
-
out << "<img#{ shelve( atts ) } />"
-
out << "</a>#{ href_a1 }#{ href_a2 }" if href
-
-
if algn
-
algn = h_align( algn )
-
if stln == "<p>"
-
out = "<p style=\"float:#{ algn }\">#{ out }"
-
else
-
out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
-
end
-
else
-
out = stln + out
-
end
-
-
out
-
end
-
end
-
-
1
def shelve( val )
-
3
@shelf << val
-
3
" :redsh##{ @shelf.length }:"
-
end
-
-
1
def retrieve( text )
-
2085
@shelf.each_with_index do |r, i|
-
3
text.gsub!( " :redsh##{ i + 1 }:", r )
-
end
-
end
-
-
1
def incoming_entities( text )
-
## turn any incoming ampersands into a dummy character for now.
-
## This uses a negative lookahead for alphanumerics followed by a semicolon,
-
## implying an incoming html entity, to be skipped
-
-
2085
text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
-
end
-
-
1
def no_textile( text )
-
2085
text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
-
'\1<notextile>\2</notextile>\3' )
-
2085
text.gsub!( /^ *==([^=]+.*?)==/m,
-
'\1<notextile>\2</notextile>\3' )
-
end
-
-
1
def clean_white_space( text )
-
# normalize line breaks
-
2085
text.gsub!( /\r\n/, "\n" )
-
2085
text.gsub!( /\r/, "\n" )
-
2085
text.gsub!( /\t/, ' ' )
-
2085
text.gsub!( /^ +$/, '' )
-
2085
text.gsub!( /\n{3,}/, "\n\n" )
-
2085
text.gsub!( /"$/, "\" " )
-
-
# if entire document is indented, flush
-
# to the left side
-
2085
flush_left text
-
end
-
-
1
def flush_left( text )
-
2085
indt = 0
-
2085
if text =~ /^ /
-
while text !~ /^ {#{indt}}\S/
-
indt += 1
-
end unless text.empty?
-
if indt.nonzero?
-
text.gsub!( /^ {#{indt}}/, '' )
-
end
-
end
-
end
-
-
1
def footnote_ref( text )
-
7352
text.gsub!( /\b\[([0-9]+?)\](\s)?/,
-
'<sup><a href="#fn\1">\1</a></sup>\2' )
-
end
-
-
1
OFFTAGS = /(code|pre|kbd|notextile)/
-
1
OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\W|\Z)/mi
-
1
OFFTAG_OPEN = /<#{ OFFTAGS }/
-
1
OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
-
1
HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
-
1
ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
-
-
1
def glyphs_textile( text, level = 0 )
-
8434
if text !~ HASTAG_MATCH
-
7352
pgl text
-
7352
footnote_ref text
-
else
-
1082
codepre = 0
-
1082
text.gsub!( ALLTAG_MATCH ) do |line|
-
## matches are off if we're between <code>, <pre> etc.
-
11528
if $1
-
5179
if line =~ OFFTAG_OPEN
-
codepre += 1
-
elsif line =~ OFFTAG_CLOSE
-
codepre -= 1
-
codepre = 0 if codepre < 0
-
end
-
elsif codepre.zero?
-
6349
glyphs_textile( line, level + 1 )
-
else
-
htmlesc( line, :NoQuotes )
-
end
-
# p [level, codepre, line]
-
-
11528
line
-
end
-
end
-
end
-
-
1
def rip_offtags( text, escape_aftertag=true, escape_line=true )
-
2085
if text =~ /<.*>/
-
## strip and encode <pre> content
-
codepre, used_offtags = 0, {}
-
text.gsub!( OFFTAG_MATCH ) do |line|
-
if $3
-
first, offtag, aftertag = $3, $4, $5
-
codepre += 1
-
used_offtags[offtag] = true
-
if codepre - used_offtags.length > 0
-
htmlesc( line, :NoQuotes ) if escape_line
-
@pre_list.last << line
-
line = ""
-
else
-
### htmlesc is disabled between CODE tags which will be parsed with highlighter
-
### Regexp in formatter.rb is : /<code\s+class="(\w+)">\s?(.+)/m
-
### NB: some changes were made not to use $N variables, because we use "match"
-
### and it breaks following lines
-
htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(/<code\s+class="(\w+)">/)
-
line = "<redpre##{ @pre_list.length }>"
-
first.match(/<#{ OFFTAGS }([^>]*)>/)
-
tag = $1
-
$2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
-
tag << " #{$1}" if $1
-
@pre_list << "<#{ tag }>#{ aftertag }"
-
end
-
elsif $1 and codepre > 0
-
if codepre - used_offtags.length > 0
-
htmlesc( line, :NoQuotes ) if escape_line
-
@pre_list.last << line
-
line = ""
-
end
-
codepre -= 1 unless codepre.zero?
-
used_offtags = {} if codepre.zero?
-
end
-
line
-
end
-
end
-
2085
text
-
end
-
-
1
def smooth_offtags( text )
-
unless @pre_list.empty?
-
## replace <pre> content
-
text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
-
end
-
end
-
-
1
def inline( text )
-
2085
[/^inline_/, /^glyphs_/].each do |meth_re|
-
4170
@rules.each do |rule_name|
-
45870
method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
-
end
-
end
-
end
-
-
1
def h_align( text )
-
H_ALGN_VALS[text]
-
end
-
-
1
def v_align( text )
-
V_ALGN_VALS[text]
-
end
-
-
1
def textile_popup_help( name, windowW, windowH )
-
' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
-
end
-
-
# HTML cleansing stuff
-
1
BASIC_TAGS = {
-
'a' => ['href', 'title'],
-
'img' => ['src', 'alt', 'title'],
-
'br' => [],
-
'i' => nil,
-
'u' => nil,
-
'b' => nil,
-
'pre' => nil,
-
'kbd' => nil,
-
'code' => ['lang'],
-
'cite' => nil,
-
'strong' => nil,
-
'em' => nil,
-
'ins' => nil,
-
'sup' => nil,
-
'sub' => nil,
-
'del' => nil,
-
'table' => nil,
-
'tr' => nil,
-
'td' => ['colspan', 'rowspan'],
-
'th' => nil,
-
'ol' => nil,
-
'ul' => nil,
-
'li' => nil,
-
'p' => nil,
-
'h1' => nil,
-
'h2' => nil,
-
'h3' => nil,
-
'h4' => nil,
-
'h5' => nil,
-
'h6' => nil,
-
'blockquote' => ['cite']
-
}
-
-
1
def clean_html( text, tags = BASIC_TAGS )
-
text.gsub!( /<!\[CDATA\[/, '' )
-
text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
-
raw = $~
-
tag = raw[2].downcase
-
if tags.has_key? tag
-
pcs = [tag]
-
tags[tag].each do |prop|
-
['"', "'", ''].each do |q|
-
q2 = ( q != '' ? q : '\s' )
-
if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
-
attrv = $1
-
next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
-
pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
-
break
-
end
-
end
-
end if tags[tag]
-
"<#{raw[1]}#{pcs.join " "}>"
-
else
-
" "
-
end
-
end
-
end
-
-
1
ALLOWED_TAGS = %w(redpre pre code notextile)
-
-
1
def escape_html_tags(text)
-
2085
text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "<#{$1}#{'>' unless $3.blank?}" }
-
end
-
end
-
-
1
require 'redmine/access_control'
-
1
require 'redmine/menu_manager'
-
1
require 'redmine/activity'
-
1
require 'redmine/search'
-
1
require 'redmine/custom_field_format'
-
1
require 'redmine/mime_type'
-
1
require 'redmine/core_ext'
-
1
require 'redmine/themes'
-
1
require 'redmine/hook'
-
1
require 'redmine/plugin'
-
1
require 'redmine/notifiable'
-
1
require 'redmine/wiki_formatting'
-
1
require 'redmine/scm/base'
-
-
1
begin
-
1
require 'RMagick' unless Object.const_defined?(:Magick)
-
rescue LoadError
-
# RMagick is not available
-
end
-
-
1
if RUBY_VERSION < '1.9'
-
require 'fastercsv'
-
else
-
1
require 'csv'
-
1
FCSV = CSV
-
end
-
-
1
Redmine::Scm::Base.add "Subversion"
-
1
Redmine::Scm::Base.add "Darcs"
-
1
Redmine::Scm::Base.add "Mercurial"
-
1
Redmine::Scm::Base.add "Cvs"
-
1
Redmine::Scm::Base.add "Bazaar"
-
1
Redmine::Scm::Base.add "Git"
-
1
Redmine::Scm::Base.add "Filesystem"
-
-
1
Redmine::CustomFieldFormat.map do |fields|
-
1
fields.register 'string'
-
1
fields.register 'text'
-
1
fields.register 'int', :label => :label_integer
-
1
fields.register 'float'
-
1
fields.register 'list'
-
1
fields.register 'date'
-
1
fields.register 'bool', :label => :label_boolean
-
1
fields.register 'user', :only => %w(Issue TimeEntry Version Project), :edit_as => 'list'
-
1
fields.register 'version', :only => %w(Issue TimeEntry Version Project), :edit_as => 'list'
-
end
-
-
# Permissions
-
1
Redmine::AccessControl.map do |map|
-
1
map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
-
1
map.permission :search_project, {:search => :index}, :public => true, :read => true
-
1
map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
-
1
map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
-
1
map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true
-
1
map.permission :select_project_modules, {:projects => :modules}, :require => :member
-
1
map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :create, :update, :destroy, :autocomplete]}, :require => :member
-
1
map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
-
1
map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
-
-
1
map.project_module :issue_tracking do |map|
-
# Issue categories
-
1
map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
-
# Issues
-
1
map.permission :view_issues, {:issues => [:index, :show],
-
:auto_complete => [:issues],
-
:context_menus => [:issues],
-
:versions => [:index, :show, :status_by],
-
:journals => [:index, :diff],
-
:queries => :index,
-
:reports => [:issue_report, :issue_report_details]},
-
:read => true
-
1
map.permission :add_issues, {:issues => [:new, :create, :update_form], :attachments => :upload}
-
1
map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new], :attachments => :upload}
-
1
map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
-
1
map.permission :manage_subtasks, {}
-
1
map.permission :set_issues_private, {}
-
1
map.permission :set_own_issues_private, {}, :require => :loggedin
-
1
map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload}
-
1
map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
-
1
map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
-
1
map.permission :view_private_notes, {}, :read => true, :require => :member
-
1
map.permission :set_notes_private, {}, :require => :member
-
1
map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin
-
1
map.permission :delete_issues, {:issues => :destroy}, :require => :member
-
# Queries
-
1
map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
-
1
map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
-
# Watchers
-
1
map.permission :view_issue_watchers, {}, :read => true
-
1
map.permission :add_issue_watchers, {:watchers => :new}
-
1
map.permission :delete_issue_watchers, {:watchers => :destroy}
-
end
-
-
1
map.project_module :time_tracking do |map|
-
1
map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
-
1
map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
-
1
map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
-
1
map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
-
1
map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
-
end
-
-
1
map.project_module :news do |map|
-
1
map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
-
1
map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true
-
1
map.permission :comment_news, {:comments => :create}
-
end
-
-
1
map.project_module :documents do |map|
-
1
map.permission :manage_documents, {:documents => [:new, :create, :edit, :update, :destroy, :add_attachment]}, :require => :loggedin
-
1
map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true
-
end
-
-
1
map.project_module :files do |map|
-
1
map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
-
1
map.permission :view_files, {:files => :index, :versions => :download}, :read => true
-
end
-
-
1
map.project_module :wiki do |map|
-
1
map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
-
1
map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
-
1
map.permission :delete_wiki_pages, {:wiki => [:destroy, :destroy_version]}, :require => :member
-
1
map.permission :view_wiki_pages, {:wiki => [:index, :show, :special, :date_index]}, :read => true
-
1
map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true
-
1
map.permission :view_wiki_edits, {:wiki => [:history, :diff, :annotate]}, :read => true
-
1
map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
-
1
map.permission :delete_wiki_pages_attachments, {}
-
1
map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
-
end
-
-
1
map.project_module :repository do |map|
-
1
map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
-
1
map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true
-
1
map.permission :view_changesets, {:repositories => [:show, :revisions, :revision]}, :read => true
-
1
map.permission :commit_access, {}
-
1
map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
-
end
-
-
1
map.project_module :boards do |map|
-
1
map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
-
1
map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true, :read => true
-
1
map.permission :add_messages, {:messages => [:new, :reply, :quote]}
-
1
map.permission :edit_messages, {:messages => :edit}, :require => :member
-
1
map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
-
1
map.permission :delete_messages, {:messages => :destroy}, :require => :member
-
1
map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
-
end
-
-
1
map.project_module :calendar do |map|
-
1
map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true
-
end
-
-
1
map.project_module :gantt do |map|
-
1
map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true
-
end
-
end
-
-
1
Redmine::MenuManager.map :top_menu do |menu|
-
1
menu.push :home, :home_path
-
375
menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
-
1
menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
-
375
menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
-
1
menu.push :help, Redmine::Info.help_url, :last => true
-
end
-
-
1
Redmine::MenuManager.map :account_menu do |menu|
-
375
menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
-
375
menu.push :register, :register_path, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
-
375
menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
-
375
menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
-
end
-
-
1
Redmine::MenuManager.map :application_menu do |menu|
-
# Empty
-
end
-
-
1
Redmine::MenuManager.map :admin_menu do |menu|
-
1
menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
-
1
menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
-
1
menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
-
1
menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
-
1
menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
-
1
menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
-
:html => {:class => 'issue_statuses'}
-
1
menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
-
1
menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
-
:html => {:class => 'custom_fields'}
-
1
menu.push :enumerations, {:controller => 'enumerations'}
-
1
menu.push :settings, {:controller => 'settings'}
-
1
menu.push :ldap_authentication, {:controller => 'auth_sources', :action => 'index'},
-
:html => {:class => 'server_authentication'}
-
1
menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
-
1
menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
-
end
-
-
1
Redmine::MenuManager.map :project_menu do |menu|
-
1
menu.push :overview, { :controller => 'projects', :action => 'show' }
-
1
menu.push :activity, { :controller => 'activities', :action => 'index' }
-
1
menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
-
123
:if => Proc.new { |p| p.shared_versions.any? }
-
1
menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
-
1
menu.push :new_issue, { :controller => 'issues', :action => 'new', :copy_from => nil }, :param => :project_id, :caption => :label_issue_new,
-
:html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
-
1
menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
-
1
menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
-
1
menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
-
1
menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
-
1
menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
-
123
:if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
-
1
menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
-
123
:if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
-
1
menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
-
1
menu.push :repository, { :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil },
-
123
:if => Proc.new { |p| p.repository && !p.repository.new_record? }
-
1
menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
-
end
-
-
1
Redmine::Activity.map do |activity|
-
1
activity.register :issues, :class_name => %w(Issue Journal)
-
1
activity.register :changesets
-
1
activity.register :news
-
1
activity.register :documents, :class_name => %w(Document Attachment)
-
1
activity.register :files, :class_name => 'Attachment'
-
1
activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
-
1
activity.register :messages, :default => false
-
1
activity.register :time_entries, :default => false
-
end
-
-
1
Redmine::Search.map do |search|
-
1
search.register :issues
-
1
search.register :news
-
1
search.register :documents
-
1
search.register :changesets
-
1
search.register :wiki_pages
-
1
search.register :messages
-
1
search.register :projects
-
end
-
-
1
Redmine::WikiFormatting.map do |format|
-
1
format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
-
end
-
-
1
ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module AccessControl
-
-
1
class << self
-
1
def map
-
18
mapper = Mapper.new
-
18
yield mapper
-
18
@permissions ||= []
-
18
@permissions += mapper.mapped_permissions
-
end
-
-
1
def permissions
-
84343
@permissions
-
end
-
-
# Returns the permission of given name or nil if it wasn't found
-
# Argument should be a symbol
-
1
def permission(name)
-
3260510
permissions.detect {|p| p.name == name}
-
end
-
-
# Returns the actions that are allowed by the permission of given name
-
1
def allowed_actions(permission_name)
-
82732
perm = permission(permission_name)
-
82732
perm ? perm.actions : []
-
end
-
-
1
def public_permissions
-
5023
@public_permissions ||= @permissions.select {|p| p.public?}
-
end
-
-
1
def members_only_permissions
-
@members_only_permissions ||= @permissions.select {|p| p.require_member?}
-
end
-
-
1
def loggedin_only_permissions
-
@loggedin_only_permissions ||= @permissions.select {|p| p.require_loggedin?}
-
end
-
-
1
def read_action?(action)
-
if action.is_a?(Symbol)
-
perm = permission(action)
-
!perm.nil? && perm.read?
-
else
-
s = "#{action[:controller]}/#{action[:action]}"
-
permissions.detect {|p| p.actions.include?(s) && !p.read?}.nil?
-
end
-
end
-
-
1
def available_project_modules
-
7
@available_project_modules ||= @permissions.collect(&:project_module).uniq.compact
-
end
-
-
1
def modules_permissions(modules)
-
138416
@permissions.select {|p| p.project_module.nil? || modules.include?(p.project_module.to_s)}
-
end
-
end
-
-
1
class Mapper
-
1
def initialize
-
18
@project_module = nil
-
end
-
-
1
def permission(name, hash, options={})
-
81
@permissions ||= []
-
81
options.merge!(:project_module => @project_module)
-
81
@permissions << Permission.new(name, hash, options)
-
end
-
-
1
def project_module(name, options={})
-
27
@project_module = name
-
27
yield self
-
27
@project_module = nil
-
end
-
-
1
def mapped_permissions
-
18
@permissions
-
end
-
end
-
-
1
class Permission
-
1
attr_reader :name, :actions, :project_module
-
-
1
def initialize(name, hash, options)
-
81
@name = name
-
81
@actions = []
-
81
@public = options[:public] || false
-
81
@require = options[:require]
-
81
@read = options[:read] || false
-
81
@project_module = options[:project_module]
-
81
hash.each do |controller, actions|
-
115
if actions.is_a? Array
-
305
@actions << actions.collect {|action| "#{controller}/#{action}"}
-
else
-
36
@actions << "#{controller}/#{actions}"
-
end
-
end
-
81
@actions.flatten!
-
end
-
-
1
def public?
-
81
@public
-
end
-
-
1
def require_member?
-
@require && @require == :member
-
end
-
-
1
def require_loggedin?
-
@require && (@require == :member || @require == :loggedin)
-
end
-
-
1
def read?
-
1610
@read
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module AccessKeys
-
ACCESSKEYS = {:edit => 'e',
-
:preview => 'r',
-
:quick_search => 'f',
-
:search => '4',
-
:new_issue => '7'
-
1
}.freeze unless const_defined?(:ACCESSKEYS)
-
-
1
def self.key_for(action)
-
777
ACCESSKEYS[action]
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Activity
-
-
1
mattr_accessor :available_event_types, :default_event_types, :providers
-
-
1
@@available_event_types = []
-
1
@@default_event_types = []
-
9
@@providers = Hash.new {|h,k| h[k]=[] }
-
-
1
class << self
-
1
def map(&block)
-
1
yield self
-
end
-
-
# Registers an activity provider
-
1
def register(event_type, options={})
-
8
options.assert_valid_keys(:class_name, :default)
-
-
8
event_type = event_type.to_s
-
8
providers = options[:class_name] || event_type.classify
-
8
providers = ([] << providers) unless providers.is_a?(Array)
-
-
8
@@available_event_types << event_type unless @@available_event_types.include?(event_type)
-
8
@@default_event_types << event_type unless options[:default] == false
-
8
@@providers[event_type] += providers
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Ciphering
-
1
def self.included(base)
-
2
base.extend ClassMethods
-
end
-
-
1
class << self
-
1
def encrypt_text(text)
-
if cipher_key.blank? || text.blank?
-
text
-
else
-
c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
-
iv = c.random_iv
-
c.encrypt
-
c.key = cipher_key
-
c.iv = iv
-
e = c.update(text.to_s)
-
e << c.final
-
"aes-256-cbc:" + [e, iv].map {|v| Base64.encode64(v).strip}.join('--')
-
end
-
end
-
-
1
def decrypt_text(text)
-
if text && match = text.match(/\Aaes-256-cbc:(.+)\Z/)
-
if cipher_key.blank?
-
logger.error "Attempt to decrypt a ciphered text with no cipher key configured in config/configuration.yml" if logger
-
return text
-
end
-
text = match[1]
-
c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
-
e, iv = text.split("--").map {|s| Base64.decode64(s)}
-
c.decrypt
-
c.key = cipher_key
-
c.iv = iv
-
d = c.update(e)
-
d << c.final
-
else
-
text
-
end
-
end
-
-
1
def cipher_key
-
key = Redmine::Configuration['database_cipher_key'].to_s
-
key.blank? ? nil : Digest::SHA256.hexdigest(key)
-
end
-
-
1
def logger
-
Rails.logger
-
end
-
end
-
-
1
module ClassMethods
-
1
def encrypt_all(attribute)
-
transaction do
-
all.each do |object|
-
clear = object.send(attribute)
-
object.send "#{attribute}=", clear
-
raise(ActiveRecord::Rollback) unless object.save(:validation => false)
-
end
-
end ? true : false
-
end
-
-
1
def decrypt_all(attribute)
-
transaction do
-
all.each do |object|
-
clear = object.send(attribute)
-
object.send :write_attribute, attribute, clear
-
raise(ActiveRecord::Rollback) unless object.save(:validation => false)
-
end
-
end
-
end ? true : false
-
end
-
-
1
private
-
-
# Returns the value of the given ciphered attribute
-
1
def read_ciphered_attribute(attribute)
-
Redmine::Ciphering.decrypt_text(read_attribute(attribute))
-
end
-
-
# Sets the value of the given ciphered attribute
-
1
def write_ciphered_attribute(attribute, value)
-
write_attribute(attribute, Redmine::Ciphering.encrypt_text(value))
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Configuration
-
-
# Configuration default values
-
1
@defaults = {
-
'email_delivery' => nil
-
}
-
-
1
@config = nil
-
-
1
class << self
-
# Loads the Redmine configuration file
-
# Valid options:
-
# * <tt>:file</tt>: the configuration file to load (default: config/configuration.yml)
-
# * <tt>:env</tt>: the environment to load the configuration for (default: Rails.env)
-
1
def load(options={})
-
1
filename = options[:file] || File.join(Rails.root, 'config', 'configuration.yml')
-
1
env = options[:env] || Rails.env
-
-
1
@config = @defaults.dup
-
-
1
load_deprecated_email_configuration(env)
-
1
if File.file?(filename)
-
@config.merge!(load_from_yaml(filename, env))
-
end
-
-
# Compatibility mode for those who copy email.yml over configuration.yml
-
1
%w(delivery_method smtp_settings sendmail_settings).each do |key|
-
3
if value = @config.delete(key)
-
@config['email_delivery'] ||= {}
-
@config['email_delivery'][key] = value
-
end
-
end
-
-
1
if @config['email_delivery']
-
ActionMailer::Base.perform_deliveries = true
-
@config['email_delivery'].each do |k, v|
-
v.symbolize_keys! if v.respond_to?(:symbolize_keys!)
-
ActionMailer::Base.send("#{k}=", v)
-
end
-
end
-
-
1
@config
-
end
-
-
# Returns a configuration setting
-
1
def [](name)
-
9
load unless @config
-
9
@config[name]
-
end
-
-
# Yields a block with the specified hash configuration settings
-
1
def with(settings)
-
settings.stringify_keys!
-
load unless @config
-
was = settings.keys.inject({}) {|h,v| h[v] = @config[v]; h}
-
@config.merge! settings
-
yield if block_given?
-
@config.merge! was
-
end
-
-
1
private
-
-
1
def load_from_yaml(filename, env)
-
yaml = nil
-
begin
-
yaml = YAML::load_file(filename)
-
rescue ArgumentError
-
$stderr.puts "Your Redmine configuration file located at #{filename} is not a valid YAML file and could not be loaded."
-
exit 1
-
end
-
conf = {}
-
if yaml.is_a?(Hash)
-
if yaml['default']
-
conf.merge!(yaml['default'])
-
end
-
if yaml[env]
-
conf.merge!(yaml[env])
-
end
-
else
-
$stderr.puts "Your Redmine configuration file located at #{filename} is not a valid Redmine configuration file."
-
exit 1
-
end
-
conf
-
end
-
-
1
def load_deprecated_email_configuration(env)
-
1
deprecated_email_conf = File.join(Rails.root, 'config', 'email.yml')
-
1
if File.file?(deprecated_email_conf)
-
warn "Storing outgoing emails configuration in config/email.yml is deprecated. You should now store it in config/configuration.yml using the email_delivery setting."
-
@config.merge!({'email_delivery' => load_from_yaml(deprecated_email_conf, env)})
-
end
-
end
-
end
-
end
-
end
-
4
Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) }
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module ActiveRecord
-
1
module FinderMethods
-
1
def find_ids(*args)
-
find_ids_with_associations
-
end
-
-
1
private
-
-
1
def find_ids_with_associations
-
join_dependency = construct_join_dependency_for_association_find
-
relation = construct_relation_for_association_find_ids(join_dependency)
-
rows = connection.select_all(relation, 'SQL', relation.bind_values)
-
rows.map {|row| row["id"].to_i}
-
rescue ThrowResult
-
[]
-
end
-
-
1
def construct_relation_for_association_find_ids(join_dependency)
-
relation = except(:includes, :eager_load, :preload, :select).select("#{table_name}.id")
-
apply_join_dependency(relation, join_dependency)
-
end
-
end
-
end
-
1
require File.dirname(__FILE__) + '/date/calculations'
-
-
1
class Date #:nodoc:
-
1
include Redmine::CoreExtensions::Date::Calculations
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine #:nodoc:
-
1
module CoreExtensions #:nodoc:
-
1
module Date #:nodoc:
-
# Custom date calculations
-
1
module Calculations
-
# Returns difference with specified date in months
-
1
def months_ago(date = self.class.today)
-
(date.year - self.year)*12 + (date.month - self.month)
-
end
-
-
# Returns difference with specified date in weeks
-
1
def weeks_ago(date = self.class.today)
-
(date.year - self.year)*52 + (date.cweek - self.cweek)
-
end
-
end
-
end
-
end
-
end
-
1
require File.dirname(__FILE__) + '/string/conversions'
-
1
require File.dirname(__FILE__) + '/string/inflections'
-
-
1
class String #:nodoc:
-
1
include Redmine::CoreExtensions::String::Conversions
-
1
include Redmine::CoreExtensions::String::Inflections
-
-
1
def is_binary_data?
-
( self.count( "^ -~", "^\r\n" ).fdiv(self.size) > 0.3 || self.index( "\x00" ) ) unless empty?
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine #:nodoc:
-
1
module CoreExtensions #:nodoc:
-
1
module String #:nodoc:
-
# Custom string conversions
-
1
module Conversions
-
# Parses hours format and returns a float
-
1
def to_hours
-
30
s = self.dup
-
30
s.strip!
-
30
if s =~ %r{^(\d+([.,]\d+)?)h?$}
-
28
s = $1
-
else
-
# 2:30 => 2.5
-
2
s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 }
-
# 2h30, 2h, 30m => 2.5, 2, 0.5
-
4
s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}i) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] }
-
end
-
# 2,5 => 2.5
-
30
s.gsub!(',', '.')
-
32
begin; Kernel.Float(s); rescue; nil; end
-
end
-
-
# Object#to_a removed in ruby1.9
-
1
if RUBY_VERSION > '1.9'
-
1
def to_a
-
11029
[self.dup]
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine #:nodoc:
-
1
module CoreExtensions #:nodoc:
-
1
module String #:nodoc:
-
# Custom string inflections
-
1
module Inflections
-
1
def with_leading_slash
-
starts_with?('/') ? self : "/#{ self }"
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
class CustomFieldFormat
-
1
include Redmine::I18n
-
-
1
cattr_accessor :available
-
1
@@available = {}
-
-
1
attr_accessor :name, :order, :label, :edit_as, :class_names
-
-
1
def initialize(name, options={})
-
9
self.name = name
-
9
self.label = options[:label] || "label_#{name}".to_sym
-
9
self.order = options[:order] || self.class.available_formats.size
-
9
self.edit_as = options[:edit_as] || name
-
9
self.class_names = options[:only]
-
end
-
-
1
def format(value)
-
50
send "format_as_#{name}", value
-
end
-
-
1
def format_as_date(value)
-
begin; format_date(value.to_date); rescue; value end
-
end
-
-
1
def format_as_bool(value)
-
l(value == "1" ? :general_text_Yes : :general_text_No)
-
end
-
-
1
['string','text','int','float','list'].each do |name|
-
5
define_method("format_as_#{name}") {|value|
-
50
return value
-
}
-
end
-
-
1
['user', 'version'].each do |name|
-
2
define_method("format_as_#{name}") {|value|
-
return value.blank? ? "" : name.classify.constantize.find_by_id(value.to_i).to_s
-
}
-
end
-
-
1
class << self
-
1
def map(&block)
-
1
yield self
-
end
-
-
# Registers a custom field format
-
1
def register(*args)
-
9
custom_field_format = args.first
-
9
unless custom_field_format.is_a?(Redmine::CustomFieldFormat)
-
9
custom_field_format = Redmine::CustomFieldFormat.new(*args)
-
end
-
9
@@available[custom_field_format.name] = custom_field_format unless @@available.keys.include?(custom_field_format.name)
-
end
-
-
1
def available_formats
-
10
@@available.keys
-
end
-
-
1
def find_by_name(name)
-
90
@@available[name.to_s]
-
end
-
-
1
def label_for(name)
-
format = @@available[name.to_s]
-
format.label if format
-
end
-
-
# Return an array of custom field formats which can be used in select_tag
-
1
def as_select(class_name=nil)
-
fields = @@available.values
-
fields = fields.select {|field| field.class_names.nil? || field.class_names.include?(class_name)}
-
fields.sort {|a,b|
-
a.order <=> b.order
-
}.collect {|custom_field_format|
-
[ l(custom_field_format.label), custom_field_format.name ]
-
}
-
end
-
-
1
def format_value(value, field_format)
-
50
return "" unless value && !value.empty?
-
-
50
if format_type = find_by_name(field_format)
-
50
format_type.format(value)
-
else
-
value
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module DefaultData
-
1
class DataAlreadyLoaded < Exception; end
-
-
1
module Loader
-
1
include Redmine::I18n
-
-
1
class << self
-
# Returns true if no data is already loaded in the database
-
# otherwise false
-
1
def no_data?
-
!Role.find(:first, :conditions => {:builtin => 0}) &&
-
1
!Tracker.find(:first) &&
-
!IssueStatus.find(:first) &&
-
!Enumeration.find(:first)
-
end
-
-
# Loads the default data
-
# Raises a RecordNotSaved exception if something goes wrong
-
1
def load(lang=nil)
-
raise DataAlreadyLoaded.new("Some configuration data is already loaded.") unless no_data?
-
set_language_if_valid(lang)
-
-
Role.transaction do
-
# Roles
-
manager = Role.create! :name => l(:default_role_manager),
-
:issues_visibility => 'all',
-
:position => 1
-
manager.permissions = manager.setable_permissions.collect {|p| p.name}
-
manager.save!
-
-
developer = Role.create! :name => l(:default_role_developer),
-
:position => 2,
-
:permissions => [:manage_versions,
-
:manage_categories,
-
:view_issues,
-
:add_issues,
-
:edit_issues,
-
:view_private_notes,
-
:set_notes_private,
-
:manage_issue_relations,
-
:manage_subtasks,
-
:add_issue_notes,
-
:save_queries,
-
:view_gantt,
-
:view_calendar,
-
:log_time,
-
:view_time_entries,
-
:comment_news,
-
:view_documents,
-
:view_wiki_pages,
-
:view_wiki_edits,
-
:edit_wiki_pages,
-
:delete_wiki_pages,
-
:add_messages,
-
:edit_own_messages,
-
:view_files,
-
:manage_files,
-
:browse_repository,
-
:view_changesets,
-
:commit_access,
-
:manage_related_issues]
-
-
reporter = Role.create! :name => l(:default_role_reporter),
-
:position => 3,
-
:permissions => [:view_issues,
-
:add_issues,
-
:add_issue_notes,
-
:save_queries,
-
:view_gantt,
-
:view_calendar,
-
:log_time,
-
:view_time_entries,
-
:comment_news,
-
:view_documents,
-
:view_wiki_pages,
-
:view_wiki_edits,
-
:add_messages,
-
:edit_own_messages,
-
:view_files,
-
:browse_repository,
-
:view_changesets]
-
-
Role.non_member.update_attribute :permissions, [:view_issues,
-
:add_issues,
-
:add_issue_notes,
-
:save_queries,
-
:view_gantt,
-
:view_calendar,
-
:view_time_entries,
-
:comment_news,
-
:view_documents,
-
:view_wiki_pages,
-
:view_wiki_edits,
-
:add_messages,
-
:view_files,
-
:browse_repository,
-
:view_changesets]
-
-
Role.anonymous.update_attribute :permissions, [:view_issues,
-
:view_gantt,
-
:view_calendar,
-
:view_time_entries,
-
:view_documents,
-
:view_wiki_pages,
-
:view_wiki_edits,
-
:view_files,
-
:browse_repository,
-
:view_changesets]
-
-
# Trackers
-
Tracker.create!(:name => l(:default_tracker_bug), :is_in_chlog => true, :is_in_roadmap => false, :position => 1)
-
Tracker.create!(:name => l(:default_tracker_feature), :is_in_chlog => true, :is_in_roadmap => true, :position => 2)
-
Tracker.create!(:name => l(:default_tracker_support), :is_in_chlog => false, :is_in_roadmap => false, :position => 3)
-
-
# Issue statuses
-
new = IssueStatus.create!(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :position => 1)
-
in_progress = IssueStatus.create!(:name => l(:default_issue_status_in_progress), :is_closed => false, :is_default => false, :position => 2)
-
resolved = IssueStatus.create!(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :position => 3)
-
feedback = IssueStatus.create!(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :position => 4)
-
closed = IssueStatus.create!(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :position => 5)
-
rejected = IssueStatus.create!(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :position => 6)
-
-
# Workflow
-
Tracker.find(:all).each { |t|
-
IssueStatus.find(:all).each { |os|
-
IssueStatus.find(:all).each { |ns|
-
WorkflowTransition.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
-
}
-
}
-
}
-
-
Tracker.find(:all).each { |t|
-
[new, in_progress, resolved, feedback].each { |os|
-
[in_progress, resolved, feedback, closed].each { |ns|
-
WorkflowTransition.create!(:tracker_id => t.id, :role_id => developer.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
-
}
-
}
-
}
-
-
Tracker.find(:all).each { |t|
-
[new, in_progress, resolved, feedback].each { |os|
-
[closed].each { |ns|
-
WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
-
}
-
}
-
WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id)
-
}
-
-
# Enumerations
-
IssuePriority.create!(:name => l(:default_priority_low), :position => 1)
-
IssuePriority.create!(:name => l(:default_priority_normal), :position => 2, :is_default => true)
-
IssuePriority.create!(:name => l(:default_priority_high), :position => 3)
-
IssuePriority.create!(:name => l(:default_priority_urgent), :position => 4)
-
IssuePriority.create!(:name => l(:default_priority_immediate), :position => 5)
-
-
DocumentCategory.create!(:name => l(:default_doc_category_user), :position => 1)
-
DocumentCategory.create!(:name => l(:default_doc_category_tech), :position => 2)
-
-
TimeEntryActivity.create!(:name => l(:default_activity_design), :position => 1)
-
TimeEntryActivity.create!(:name => l(:default_activity_development), :position => 2)
-
end
-
true
-
end
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'iconv'
-
1
require 'tcpdf'
-
1
require 'fpdf/chinese'
-
1
require 'fpdf/japanese'
-
1
require 'fpdf/korean'
-
-
1
module Redmine
-
1
module Export
-
1
module PDF
-
1
include ActionView::Helpers::TextHelper
-
1
include ActionView::Helpers::NumberHelper
-
1
include IssuesHelper
-
-
1
class ITCPDF < TCPDF
-
1
include Redmine::I18n
-
1
attr_accessor :footer_date
-
-
1
def initialize(lang, orientation='P')
-
@@k_path_cache = Rails.root.join('tmp', 'pdf')
-
FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache)
-
set_language_if_valid lang
-
pdf_encoding = l(:general_pdf_encoding).upcase
-
super(orientation, 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding)
-
case current_language.to_s.downcase
-
when 'vi'
-
@font_for_content = 'DejaVuSans'
-
@font_for_footer = 'DejaVuSans'
-
else
-
case pdf_encoding
-
when 'UTF-8'
-
@font_for_content = 'FreeSans'
-
@font_for_footer = 'FreeSans'
-
when 'CP949'
-
extend(PDF_Korean)
-
AddUHCFont()
-
@font_for_content = 'UHC'
-
@font_for_footer = 'UHC'
-
when 'CP932', 'SJIS', 'SHIFT_JIS'
-
extend(PDF_Japanese)
-
AddSJISFont()
-
@font_for_content = 'SJIS'
-
@font_for_footer = 'SJIS'
-
when 'GB18030'
-
extend(PDF_Chinese)
-
AddGBFont()
-
@font_for_content = 'GB'
-
@font_for_footer = 'GB'
-
when 'BIG5'
-
extend(PDF_Chinese)
-
AddBig5Font()
-
@font_for_content = 'Big5'
-
@font_for_footer = 'Big5'
-
else
-
@font_for_content = 'Arial'
-
@font_for_footer = 'Helvetica'
-
end
-
end
-
SetCreator(Redmine::Info.app_name)
-
SetFont(@font_for_content)
-
@outlines = []
-
@outlineRoot = nil
-
end
-
-
1
def SetFontStyle(style, size)
-
SetFont(@font_for_content, style, size)
-
end
-
-
1
def SetTitle(txt)
-
txt = begin
-
utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
-
hextxt = "<FEFF" # FEFF is BOM
-
hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
-
hextxt << ">"
-
rescue
-
txt
-
end || ''
-
super(txt)
-
end
-
-
1
def textstring(s)
-
# Format a text string
-
if s =~ /^</ # This means the string is hex-dumped.
-
return s
-
else
-
return '('+escape(s)+')'
-
end
-
end
-
-
1
def fix_text_encoding(txt)
-
RDMPdfEncoding::rdm_from_utf8(txt, l(:general_pdf_encoding))
-
end
-
-
1
def formatted_text(text)
-
html = Redmine::WikiFormatting.to_html(Setting.text_formatting, text)
-
# Strip {{toc}} tags
-
html.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i, '')
-
html
-
end
-
-
1
def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='')
-
Cell(w, h, fix_text_encoding(txt), border, ln, align, fill, link)
-
end
-
-
1
def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1)
-
MultiCell(w, h, fix_text_encoding(txt), border, align, fill, ln)
-
end
-
-
1
def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0)
-
@attachments = attachments
-
writeHTMLCell(w, h, x, y,
-
fix_text_encoding(formatted_text(txt)),
-
border, ln, fill)
-
end
-
-
1
def getImageFilename(attrname)
-
# attrname: general_pdf_encoding string file/uri name
-
atta = RDMPdfEncoding.attach(@attachments, attrname, l(:general_pdf_encoding))
-
if atta
-
return atta.diskfile
-
else
-
return nil
-
end
-
end
-
-
1
def Footer
-
SetFont(@font_for_footer, 'I', 8)
-
SetY(-15)
-
SetX(15)
-
RDMCell(0, 5, @footer_date, 0, 0, 'L')
-
SetY(-15)
-
SetX(-30)
-
RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
-
end
-
-
1
def Bookmark(txt, level=0, y=0)
-
if (y == -1)
-
y = GetY()
-
end
-
@outlines << {:t => txt, :l => level, :p => PageNo(), :y => (@h - y)*@k}
-
end
-
-
1
def bookmark_title(txt)
-
txt = begin
-
utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
-
hextxt = "<FEFF" # FEFF is BOM
-
hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
-
hextxt << ">"
-
rescue
-
txt
-
end || ''
-
end
-
-
1
def putbookmarks
-
nb=@outlines.size
-
return if (nb==0)
-
lru=[]
-
level=0
-
@outlines.each_with_index do |o, i|
-
if(o[:l]>0)
-
parent=lru[o[:l]-1]
-
#Set parent and last pointers
-
@outlines[i][:parent]=parent
-
@outlines[parent][:last]=i
-
if (o[:l]>level)
-
#Level increasing: set first pointer
-
@outlines[parent][:first]=i
-
end
-
else
-
@outlines[i][:parent]=nb
-
end
-
if (o[:l]<=level && i>0)
-
#Set prev and next pointers
-
prev=lru[o[:l]]
-
@outlines[prev][:next]=i
-
@outlines[i][:prev]=prev
-
end
-
lru[o[:l]]=i
-
level=o[:l]
-
end
-
#Outline items
-
n=self.n+1
-
@outlines.each_with_index do |o, i|
-
newobj()
-
out('<</Title '+bookmark_title(o[:t]))
-
out("/Parent #{n+o[:parent]} 0 R")
-
if (o[:prev])
-
out("/Prev #{n+o[:prev]} 0 R")
-
end
-
if (o[:next])
-
out("/Next #{n+o[:next]} 0 R")
-
end
-
if (o[:first])
-
out("/First #{n+o[:first]} 0 R")
-
end
-
if (o[:last])
-
out("/Last #{n+o[:last]} 0 R")
-
end
-
out("/Dest [%d 0 R /XYZ 0 %.2f null]" % [1+2*o[:p], o[:y]])
-
out('/Count 0>>')
-
out('endobj')
-
end
-
#Outline root
-
newobj()
-
@outlineRoot=self.n
-
out("<</Type /Outlines /First #{n} 0 R");
-
out("/Last #{n+lru[0]} 0 R>>");
-
out('endobj');
-
end
-
-
1
def putresources()
-
super
-
putbookmarks()
-
end
-
-
1
def putcatalog()
-
super
-
if(@outlines.size > 0)
-
out("/Outlines #{@outlineRoot} 0 R");
-
out('/PageMode /UseOutlines');
-
end
-
end
-
end
-
-
# fetch row values
-
1
def fetch_row_values(issue, query, level)
-
query.inline_columns.collect do |column|
-
s = if column.is_a?(QueryCustomFieldColumn)
-
cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
-
show_value(cv)
-
else
-
value = issue.send(column.name)
-
if column.name == :subject
-
value = " " * level + value
-
end
-
if value.is_a?(Date)
-
format_date(value)
-
elsif value.is_a?(Time)
-
format_time(value)
-
else
-
value
-
end
-
end
-
s.to_s
-
end
-
end
-
-
# calculate columns width
-
1
def calc_col_width(issues, query, table_width, pdf)
-
# calculate statistics
-
# by captions
-
pdf.SetFontStyle('B',8)
-
col_padding = pdf.GetStringWidth('OO')
-
col_width_min = query.inline_columns.map {|v| pdf.GetStringWidth(v.caption) + col_padding}
-
col_width_max = Array.new(col_width_min)
-
col_width_avg = Array.new(col_width_min)
-
word_width_max = query.inline_columns.map {|c|
-
n = 10
-
c.caption.split.each {|w|
-
x = pdf.GetStringWidth(w) + col_padding
-
n = x if n < x
-
}
-
n
-
}
-
-
# by properties of issues
-
pdf.SetFontStyle('',8)
-
col_padding = pdf.GetStringWidth('OO')
-
k = 1
-
issue_list(issues) {|issue, level|
-
k += 1
-
values = fetch_row_values(issue, query, level)
-
values.each_with_index {|v,i|
-
n = pdf.GetStringWidth(v) + col_padding
-
col_width_max[i] = n if col_width_max[i] < n
-
col_width_min[i] = n if col_width_min[i] > n
-
col_width_avg[i] += n
-
v.split.each {|w|
-
x = pdf.GetStringWidth(w) + col_padding
-
word_width_max[i] = x if word_width_max[i] < x
-
}
-
}
-
}
-
col_width_avg.map! {|x| x / k}
-
-
# calculate columns width
-
ratio = table_width / col_width_avg.inject(0) {|s,w| s += w}
-
col_width = col_width_avg.map {|w| w * ratio}
-
-
# correct max word width if too many columns
-
ratio = table_width / word_width_max.inject(0) {|s,w| s += w}
-
word_width_max.map! {|v| v * ratio} if ratio < 1
-
-
# correct and lock width of some columns
-
done = 1
-
col_fix = []
-
col_width.each_with_index do |w,i|
-
if w > col_width_max[i]
-
col_width[i] = col_width_max[i]
-
col_fix[i] = 1
-
done = 0
-
elsif w < word_width_max[i]
-
col_width[i] = word_width_max[i]
-
col_fix[i] = 1
-
done = 0
-
else
-
col_fix[i] = 0
-
end
-
end
-
-
# iterate while need to correct and lock coluns width
-
while done == 0
-
# calculate free & locked columns width
-
done = 1
-
fix_col_width = 0
-
free_col_width = 0
-
col_width.each_with_index do |w,i|
-
if col_fix[i] == 1
-
fix_col_width += w
-
else
-
free_col_width += w
-
end
-
end
-
-
# calculate column normalizing ratio
-
if free_col_width == 0
-
ratio = table_width / col_width.inject(0) {|s,w| s += w}
-
else
-
ratio = (table_width - fix_col_width) / free_col_width
-
end
-
-
# correct columns width
-
col_width.each_with_index do |w,i|
-
if col_fix[i] == 0
-
col_width[i] = w * ratio
-
-
# check if column width less then max word width
-
if col_width[i] < word_width_max[i]
-
col_width[i] = word_width_max[i]
-
col_fix[i] = 1
-
done = 0
-
elsif col_width[i] > col_width_max[i]
-
col_width[i] = col_width_max[i]
-
col_fix[i] = 1
-
done = 0
-
end
-
end
-
end
-
end
-
col_width
-
end
-
-
1
def render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
-
# headers
-
pdf.SetFontStyle('B',8)
-
pdf.SetFillColor(230, 230, 230)
-
-
# render it background to find the max height used
-
base_x = pdf.GetX
-
base_y = pdf.GetY
-
max_height = issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true)
-
pdf.Rect(base_x, base_y, table_width + col_id_width, max_height, 'FD');
-
pdf.SetXY(base_x, base_y);
-
-
# write the cells on page
-
pdf.RDMCell(col_id_width, row_height, "#", "T", 0, 'C', 1)
-
issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true)
-
issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
-
pdf.SetY(base_y + max_height);
-
-
# rows
-
pdf.SetFontStyle('',8)
-
pdf.SetFillColor(255, 255, 255)
-
end
-
-
# Returns a PDF string of a list of issues
-
1
def issues_to_pdf(issues, project, query)
-
pdf = ITCPDF.new(current_language, "L")
-
title = query.new_record? ? l(:label_issue_plural) : query.name
-
title = "#{project} - #{title}" if project
-
pdf.SetTitle(title)
-
pdf.alias_nb_pages
-
pdf.footer_date = format_date(Date.today)
-
pdf.SetAutoPageBreak(false)
-
pdf.AddPage("L")
-
-
# Landscape A4 = 210 x 297 mm
-
page_height = 210
-
page_width = 297
-
right_margin = 10
-
bottom_margin = 20
-
col_id_width = 10
-
row_height = 4
-
-
# column widths
-
table_width = page_width - right_margin - 10 # fixed left margin
-
col_width = []
-
unless query.inline_columns.empty?
-
col_width = calc_col_width(issues, query, table_width - col_id_width, pdf)
-
table_width = col_width.inject(0) {|s,v| s += v}
-
end
-
-
# use full width if the description is displayed
-
if table_width > 0 && query.has_column?(:description)
-
col_width = col_width.map {|w| w = w * (page_width - right_margin - 10 - col_id_width) / table_width}
-
table_width = col_width.inject(0) {|s,v| s += v}
-
end
-
-
# title
-
pdf.SetFontStyle('B',11)
-
pdf.RDMCell(190,10, title)
-
pdf.Ln
-
render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
-
previous_group = false
-
issue_list(issues) do |issue, level|
-
if query.grouped? &&
-
(group = query.group_by_column.value(issue)) != previous_group
-
pdf.SetFontStyle('B',10)
-
group_label = group.blank? ? 'None' : group.to_s.dup
-
group_label << " (#{query.issue_count_by_group[group]})"
-
pdf.Bookmark group_label, 0, -1
-
pdf.RDMCell(table_width + col_id_width, row_height * 2, group_label, 1, 1, 'L')
-
pdf.SetFontStyle('',8)
-
previous_group = group
-
end
-
-
# fetch row values
-
col_values = fetch_row_values(issue, query, level)
-
-
# render it off-page to find the max height used
-
base_x = pdf.GetX
-
base_y = pdf.GetY
-
pdf.SetY(2 * page_height)
-
max_height = issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
-
pdf.SetXY(base_x, base_y)
-
-
# make new page if it doesn't fit on the current one
-
space_left = page_height - base_y - bottom_margin
-
if max_height > space_left
-
pdf.AddPage("L")
-
render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
-
base_x = pdf.GetX
-
base_y = pdf.GetY
-
end
-
-
# write the cells on page
-
pdf.RDMCell(col_id_width, row_height, issue.id.to_s, "T", 0, 'C', 1)
-
issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
-
issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
-
pdf.SetY(base_y + max_height);
-
-
if query.has_column?(:description) && issue.description?
-
pdf.SetX(10)
-
pdf.SetAutoPageBreak(true, 20)
-
pdf.RDMwriteHTMLCell(0, 5, 10, 0, issue.description.to_s, issue.attachments, "LRBT")
-
pdf.SetAutoPageBreak(false)
-
end
-
end
-
-
if issues.size == Setting.issues_export_limit.to_i
-
pdf.SetFontStyle('B',10)
-
pdf.RDMCell(0, row_height, '...')
-
end
-
pdf.Output
-
end
-
-
# Renders MultiCells and returns the maximum height used
-
1
def issues_to_pdf_write_cells(pdf, col_values, col_widths,
-
row_height, head=false)
-
base_y = pdf.GetY
-
max_height = row_height
-
col_values.each_with_index do |column, i|
-
col_x = pdf.GetX
-
if head == true
-
pdf.RDMMultiCell(col_widths[i], row_height, column.caption, "T", 'L', 1)
-
else
-
pdf.RDMMultiCell(col_widths[i], row_height, column, "T", 'L', 1)
-
end
-
max_height = (pdf.GetY - base_y) if (pdf.GetY - base_y) > max_height
-
pdf.SetXY(col_x + col_widths[i], base_y);
-
end
-
return max_height
-
end
-
-
# Draw lines to close the row (MultiCell border drawing in not uniform)
-
1
def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y,
-
id_width, col_widths)
-
col_x = top_x + id_width
-
pdf.Line(col_x, top_y, col_x, lower_y) # id right border
-
col_widths.each do |width|
-
col_x += width
-
pdf.Line(col_x, top_y, col_x, lower_y) # columns right border
-
end
-
pdf.Line(top_x, top_y, top_x, lower_y) # left border
-
pdf.Line(top_x, lower_y, col_x, lower_y) # bottom border
-
end
-
-
# Returns a PDF string of a single issue
-
1
def issue_to_pdf(issue, assoc={})
-
pdf = ITCPDF.new(current_language)
-
pdf.SetTitle("#{issue.project} - #{issue.tracker} ##{issue.id}")
-
pdf.alias_nb_pages
-
pdf.footer_date = format_date(Date.today)
-
pdf.AddPage
-
pdf.SetFontStyle('B',11)
-
buf = "#{issue.project} - #{issue.tracker} ##{issue.id}"
-
pdf.RDMMultiCell(190, 5, buf)
-
pdf.SetFontStyle('',8)
-
base_x = pdf.GetX
-
i = 1
-
issue.ancestors.visible.each do |ancestor|
-
pdf.SetX(base_x + i)
-
buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
-
pdf.RDMMultiCell(190 - i, 5, buf)
-
i += 1 if i < 35
-
end
-
pdf.SetFontStyle('B',11)
-
pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s)
-
pdf.SetFontStyle('',8)
-
pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
-
pdf.Ln
-
-
left = []
-
left << [l(:field_status), issue.status]
-
left << [l(:field_priority), issue.priority]
-
left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
-
left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id')
-
left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
-
-
right = []
-
right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
-
right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
-
right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
-
right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
-
right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
-
-
rows = left.size > right.size ? left.size : right.size
-
while left.size < rows
-
left << nil
-
end
-
while right.size < rows
-
right << nil
-
end
-
-
half = (issue.custom_field_values.size / 2.0).ceil
-
issue.custom_field_values.each_with_index do |custom_value, i|
-
(i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value)]
-
end
-
-
rows = left.size > right.size ? left.size : right.size
-
rows.times do |i|
-
item = left[i]
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L")
-
pdf.SetFontStyle('',9)
-
pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R")
-
-
item = right[i]
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L")
-
pdf.SetFontStyle('',9)
-
pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R")
-
pdf.Ln
-
end
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
-
pdf.SetFontStyle('',9)
-
-
# Set resize image scale
-
pdf.SetImageScale(1.6)
-
pdf.RDMwriteHTMLCell(35+155, 5, 0, 0,
-
issue.description.to_s, issue.attachments, "LRB")
-
-
unless issue.leaf?
-
# for CJK
-
truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 90 : 65 )
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR")
-
pdf.Ln
-
issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
-
buf = truncate("#{child.tracker} # #{child.id}: #{child.subject}",
-
:length => truncate_length)
-
level = 10 if level >= 10
-
pdf.SetFontStyle('',8)
-
pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, "L")
-
pdf.SetFontStyle('B',8)
-
pdf.RDMCell(20,5, child.status.to_s, "R")
-
pdf.Ln
-
end
-
end
-
-
relations = issue.relations.select { |r| r.other_issue(issue).visible? }
-
unless relations.empty?
-
# for CJK
-
truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 80 : 60 )
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR")
-
pdf.Ln
-
relations.each do |relation|
-
buf = ""
-
buf += "#{l(relation.label_for(issue))} "
-
if relation.delay && relation.delay != 0
-
buf += "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)}) "
-
end
-
if Setting.cross_project_issue_relations?
-
buf += "#{relation.other_issue(issue).project} - "
-
end
-
buf += "#{relation.other_issue(issue).tracker}" +
-
" # #{relation.other_issue(issue).id}: #{relation.other_issue(issue).subject}"
-
buf = truncate(buf, :length => truncate_length)
-
pdf.SetFontStyle('', 8)
-
pdf.RDMCell(35+155-60, 5, buf, "L")
-
pdf.SetFontStyle('B',8)
-
pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "")
-
pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "")
-
pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), "R")
-
pdf.Ln
-
end
-
end
-
pdf.RDMCell(190,5, "", "T")
-
pdf.Ln
-
-
if issue.changesets.any? &&
-
User.current.allowed_to?(:view_changesets, issue.project)
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
-
pdf.Ln
-
for changeset in issue.changesets
-
pdf.SetFontStyle('B',8)
-
csstr = "#{l(:label_revision)} #{changeset.format_identifier} - "
-
csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s
-
pdf.RDMCell(190, 5, csstr)
-
pdf.Ln
-
unless changeset.comments.blank?
-
pdf.SetFontStyle('',8)
-
pdf.RDMwriteHTMLCell(190,5,0,0,
-
changeset.comments.to_s, issue.attachments, "")
-
end
-
pdf.Ln
-
end
-
end
-
-
if assoc[:journals].present?
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(190,5, l(:label_history), "B")
-
pdf.Ln
-
assoc[:journals].each do |journal|
-
pdf.SetFontStyle('B',8)
-
title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}"
-
title << " (#{l(:field_private_notes)})" if journal.private_notes?
-
pdf.RDMCell(190,5, title)
-
pdf.Ln
-
pdf.SetFontStyle('I',8)
-
details_to_strings(journal.details, true).each do |string|
-
pdf.RDMMultiCell(190,5, "- " + string)
-
end
-
if journal.notes?
-
pdf.Ln unless journal.details.empty?
-
pdf.SetFontStyle('',8)
-
pdf.RDMwriteHTMLCell(190,5,0,0,
-
journal.notes.to_s, issue.attachments, "")
-
end
-
pdf.Ln
-
end
-
end
-
-
if issue.attachments.any?
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
-
pdf.Ln
-
for attachment in issue.attachments
-
pdf.SetFontStyle('',8)
-
pdf.RDMCell(80,5, attachment.filename)
-
pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
-
pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
-
pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
-
pdf.Ln
-
end
-
end
-
pdf.Output
-
end
-
-
# Returns a PDF string of a set of wiki pages
-
1
def wiki_pages_to_pdf(pages, project)
-
pdf = ITCPDF.new(current_language)
-
pdf.SetTitle(project.name)
-
pdf.alias_nb_pages
-
pdf.footer_date = format_date(Date.today)
-
pdf.AddPage
-
pdf.SetFontStyle('B',11)
-
pdf.RDMMultiCell(190,5, project.name)
-
pdf.Ln
-
# Set resize image scale
-
pdf.SetImageScale(1.6)
-
pdf.SetFontStyle('',9)
-
write_page_hierarchy(pdf, pages.group_by(&:parent_id))
-
pdf.Output
-
end
-
-
# Returns a PDF string of a single wiki page
-
1
def wiki_page_to_pdf(page, project)
-
pdf = ITCPDF.new(current_language)
-
pdf.SetTitle("#{project} - #{page.title}")
-
pdf.alias_nb_pages
-
pdf.footer_date = format_date(Date.today)
-
pdf.AddPage
-
pdf.SetFontStyle('B',11)
-
pdf.RDMMultiCell(190,5,
-
"#{project} - #{page.title} - # #{page.content.version}")
-
pdf.Ln
-
# Set resize image scale
-
pdf.SetImageScale(1.6)
-
pdf.SetFontStyle('',9)
-
write_wiki_page(pdf, page)
-
pdf.Output
-
end
-
-
1
def write_page_hierarchy(pdf, pages, node=nil, level=0)
-
if pages[node]
-
pages[node].each do |page|
-
if @new_page
-
pdf.AddPage
-
else
-
@new_page = true
-
end
-
pdf.Bookmark page.title, level
-
write_wiki_page(pdf, page)
-
write_page_hierarchy(pdf, pages, page.id, level + 1) if pages[page.id]
-
end
-
end
-
end
-
-
1
def write_wiki_page(pdf, page)
-
pdf.RDMwriteHTMLCell(190,5,0,0,
-
page.content.text.to_s, page.attachments, 0)
-
if page.attachments.any?
-
pdf.Ln
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
-
pdf.Ln
-
for attachment in page.attachments
-
pdf.SetFontStyle('',8)
-
pdf.RDMCell(80,5, attachment.filename)
-
pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
-
pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
-
pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
-
pdf.Ln
-
end
-
end
-
end
-
-
1
class RDMPdfEncoding
-
1
def self.rdm_from_utf8(txt, encoding)
-
txt ||= ''
-
txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
-
if txt.respond_to?(:force_encoding)
-
txt.force_encoding('ASCII-8BIT')
-
end
-
txt
-
end
-
-
1
def self.attach(attachments, filename, encoding)
-
filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
-
atta = nil
-
if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
-
atta = Attachment.latest_attach(attachments, filename_utf8)
-
end
-
if atta && atta.readable? && atta.visible?
-
return atta
-
else
-
return nil
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Helpers
-
-
# Simple class to compute the start and end dates of a calendar
-
1
class Calendar
-
1
include Redmine::I18n
-
1
attr_reader :startdt, :enddt
-
-
1
def initialize(date, lang = current_language, period = :month)
-
1
@date = date
-
1
@events = []
-
1
@ending_events_by_days = {}
-
1
@starting_events_by_days = {}
-
1
set_language_if_valid lang
-
1
case period
-
when :month
-
@startdt = Date.civil(date.year, date.month, 1)
-
@enddt = (@startdt >> 1)-1
-
# starts from the first day of the week
-
@startdt = @startdt - (@startdt.cwday - first_wday)%7
-
# ends on the last day of the week
-
@enddt = @enddt + (last_wday - @enddt.cwday)%7
-
when :week
-
1
@startdt = date - (date.cwday - first_wday)%7
-
1
@enddt = date + (last_wday - date.cwday)%7
-
else
-
raise 'Invalid period'
-
end
-
end
-
-
# Sets calendar events
-
1
def events=(events)
-
1
@events = events
-
4
@ending_events_by_days = @events.group_by {|event| event.due_date}
-
4
@starting_events_by_days = @events.group_by {|event| event.start_date}
-
end
-
-
# Returns events for the given day
-
1
def events_on(day)
-
7
((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq
-
end
-
-
# Calendar current month
-
1
def month
-
7
@date.month
-
end
-
-
# Return the first day of week
-
# 1 = Monday ... 7 = Sunday
-
1
def first_wday
-
16
case Setting.start_of_week.to_i
-
when 1
-
@first_dow ||= (1 - 1)%7 + 1
-
when 6
-
@first_dow ||= (6 - 1)%7 + 1
-
when 7
-
@first_dow ||= (7 - 1)%7 + 1
-
else
-
16
@first_dow ||= (l(:general_first_day_of_week).to_i - 1)%7 + 1
-
end
-
end
-
-
1
def last_wday
-
8
@last_dow ||= (first_wday + 5)%7 + 1
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Helpers
-
1
class Diff
-
1
include ERB::Util
-
1
include ActionView::Helpers::TagHelper
-
1
include ActionView::Helpers::TextHelper
-
1
attr_reader :diff, :words
-
-
1
def initialize(content_to, content_from)
-
@words = content_to.to_s.split(/(\s+)/)
-
@words = @words.select {|word| word != ' '}
-
words_from = content_from.to_s.split(/(\s+)/)
-
words_from = words_from.select {|word| word != ' '}
-
@diff = words_from.diff @words
-
end
-
-
1
def to_html
-
words = self.words.collect{|word| h(word)}
-
words_add = 0
-
words_del = 0
-
dels = 0
-
del_off = 0
-
diff.diffs.each do |diff|
-
add_at = nil
-
add_to = nil
-
del_at = nil
-
deleted = ""
-
diff.each do |change|
-
pos = change[1]
-
if change[0] == "+"
-
add_at = pos + dels unless add_at
-
add_to = pos + dels
-
words_add += 1
-
else
-
del_at = pos unless del_at
-
deleted << ' ' unless deleted.empty?
-
deleted << h(change[2])
-
words_del += 1
-
end
-
end
-
if add_at
-
words[add_at] = '<span class="diff_in">'.html_safe + words[add_at]
-
words[add_to] = words[add_to] + '</span>'.html_safe
-
end
-
if del_at
-
words.insert del_at - del_off + dels + words_add, '<span class="diff_out">'.html_safe + deleted + '</span>'.html_safe
-
dels += 1
-
del_off += words_del
-
words_del = 0
-
end
-
end
-
words.join(' ').html_safe
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Hook
-
1
@@listener_classes = []
-
1
@@listeners = nil
-
1
@@hook_listeners = {}
-
-
1
class << self
-
# Adds a listener class.
-
# Automatically called when a class inherits from Redmine::Hook::Listener.
-
1
def add_listener(klass)
-
2
raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton)
-
2
@@listener_classes << klass
-
2
clear_listeners_instances
-
end
-
-
# Returns all the listerners instances.
-
1
def listeners
-
34
@@listeners ||= @@listener_classes.collect {|listener| listener.instance}
-
end
-
-
# Returns the listeners instances for the given hook.
-
1
def hook_listeners(hook)
-
2901
@@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
-
end
-
-
# Clears all the listeners.
-
1
def clear_listeners
-
@@listener_classes = []
-
clear_listeners_instances
-
end
-
-
# Clears all the listeners instances.
-
1
def clear_listeners_instances
-
2
@@listeners = nil
-
2
@@hook_listeners = {}
-
end
-
-
# Calls a hook.
-
# Returns the listeners response.
-
1
def call_hook(hook, context={})
-
2837
[].tap do |response|
-
2837
hls = hook_listeners(hook)
-
2837
if hls.any?
-
1006
hls.each {|listener| response << listener.send(hook, context)}
-
end
-
end
-
end
-
end
-
-
# Base class for hook listeners.
-
1
class Listener
-
1
include Singleton
-
1
include Redmine::I18n
-
-
# Registers the listener
-
1
def self.inherited(child)
-
2
Redmine::Hook.add_listener(child)
-
2
super
-
end
-
-
end
-
-
# Listener class used for views hooks.
-
# Listeners that inherit this class will include various helpers by default.
-
1
class ViewListener < Listener
-
1
include ERB::Util
-
1
include ActionView::Helpers::TagHelper
-
1
include ActionView::Helpers::FormHelper
-
1
include ActionView::Helpers::FormTagHelper
-
1
include ActionView::Helpers::FormOptionsHelper
-
1
include ActionView::Helpers::JavaScriptHelper
-
1
include ActionView::Helpers::NumberHelper
-
1
include ActionView::Helpers::UrlHelper
-
1
include ActionView::Helpers::AssetTagHelper
-
1
include ActionView::Helpers::TextHelper
-
1
include Rails.application.routes.url_helpers
-
1
include ApplicationHelper
-
-
# Default to creating links using only the path. Subclasses can
-
# change this default as needed
-
1
def self.default_url_options
-
20
{:only_path => true }
-
end
-
-
# Helper method to directly render a partial using the context:
-
#
-
# class MyHook < Redmine::Hook::ViewListener
-
# render_on :view_issues_show_details_bottom, :partial => "show_more_data"
-
# end
-
#
-
1
def self.render_on(hook, options={})
-
define_method hook do |context|
-
if context[:hook_caller].respond_to?(:render)
-
context[:hook_caller].send(:render, {:locals => context}.merge(options))
-
elsif context[:controller].is_a?(ActionController::Base)
-
context[:controller].send(:render_to_string, {:locals => context}.merge(options))
-
else
-
raise "Cannot render #{self.name} hook from #{context[:hook_caller].class.name}"
-
end
-
end
-
end
-
-
1
def controller
-
nil
-
end
-
-
1
def config
-
ActionController::Base.config
-
end
-
end
-
-
# Helper module included in ApplicationHelper and ActionController so that
-
# hooks can be called in views like this:
-
#
-
# <%= call_hook(:some_hook) %>
-
# <%= call_hook(:another_hook, :foo => 'bar') %>
-
#
-
# Or in controllers like:
-
# call_hook(:some_hook)
-
# call_hook(:another_hook, :foo => 'bar')
-
#
-
# Hooks added to views will be concatenated into a string. Hooks added to
-
# controllers will return an array of results.
-
#
-
# Several objects are automatically added to the call context:
-
#
-
# * project => current project
-
# * request => Request instance
-
# * controller => current Controller instance
-
# * hook_caller => object that called the hook
-
#
-
1
module Helper
-
1
def call_hook(hook, context={})
-
2837
if is_a?(ActionController::Base)
-
127
default_context = {:controller => self, :project => @project, :request => request, :hook_caller => self}
-
127
Redmine::Hook.call_hook(hook, default_context.merge(context))
-
else
-
2710
default_context = { :project => @project, :hook_caller => self }
-
2710
default_context[:controller] = controller if respond_to?(:controller)
-
2710
default_context[:request] = request if respond_to?(:request)
-
2710
Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ').html_safe
-
end
-
end
-
end
-
end
-
end
-
-
1
ApplicationHelper.send(:include, Redmine::Hook::Helper)
-
1
ActionController::Base.send(:include, Redmine::Hook::Helper)
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module I18n
-
1
def self.included(base)
-
16
base.extend Redmine::I18n
-
end
-
-
1
def l(*args)
-
35961
case args.size
-
when 1
-
32544
::I18n.t(*args)
-
when 2
-
3417
if args.last.is_a?(Hash)
-
3265
::I18n.t(*args)
-
152
elsif args.last.is_a?(String)
-
140
::I18n.t(args.first, :value => args.last)
-
else
-
12
::I18n.t(args.first, :count => args.last)
-
end
-
else
-
raise "Translation string with multiple values: #{args.first}"
-
end
-
end
-
-
1
def l_or_humanize(s, options={})
-
2555
k = "#{options[:prefix]}#{s}".to_sym
-
2555
::I18n.t(k, :default => s.to_s.humanize)
-
end
-
-
1
def l_hours(hours)
-
72
hours = hours.to_f
-
72
l((hours < 2.0 ? :label_f_hour : :label_f_hour_plural), :value => ("%.2f" % hours.to_f))
-
end
-
-
1
def ll(lang, str, value=nil)
-
::I18n.t(str.to_s, :value => value, :locale => lang.to_s.gsub(%r{(.+)\-(.+)$}) { "#{$1}-#{$2.upcase}" })
-
end
-
-
1
def format_date(date)
-
392
return nil unless date
-
355
options = {}
-
355
options[:format] = Setting.date_format unless Setting.date_format.blank?
-
355
options[:locale] = User.current.language unless User.current.language.blank?
-
355
::I18n.l(date.to_date, options)
-
end
-
-
1
def format_time(time, include_date = true)
-
236
return nil unless time
-
236
options = {}
-
236
options[:format] = (Setting.time_format.blank? ? :time : Setting.time_format)
-
236
options[:locale] = User.current.language unless User.current.language.blank?
-
236
time = time.to_time if time.is_a?(String)
-
236
zone = User.current.time_zone
-
236
local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
-
236
(include_date ? "#{format_date(local)} " : "") + ::I18n.l(local, options)
-
end
-
-
1
def day_name(day)
-
7
::I18n.t('date.day_names')[day % 7]
-
end
-
-
1
def day_letter(day)
-
::I18n.t('date.abbr_day_names')[day % 7].first
-
end
-
-
1
def month_name(month)
-
::I18n.t('date.month_names')[month]
-
end
-
-
1
def valid_languages
-
3527
::I18n.available_locales
-
end
-
-
# Returns an array of languages names and code sorted by names, example:
-
# [["Deutsch", "de"], ["English", "en"] ...]
-
#
-
# The result is cached to prevent from loading all translations files.
-
1
def languages_options
-
ActionController::Base.cache_store.fetch "i18n/languages_options" do
-
valid_languages.map {|lang| [ll(lang.to_s, :general_lang_name), lang.to_s]}.sort {|x,y| x.first <=> y.first }
-
end
-
end
-
-
1
def find_language(lang)
-
169296
@@languages_lookup = valid_languages.inject({}) {|k, v| k[v.to_s.downcase] = v; k }
-
3527
@@languages_lookup[lang.to_s.downcase]
-
end
-
-
1
def set_language_if_valid(lang)
-
2850
if l = find_language(lang)
-
2850
::I18n.locale = l
-
end
-
end
-
-
1
def current_language
-
1050
::I18n.locale
-
end
-
-
# Custom backend based on I18n::Backend::Simple with the following changes:
-
# * lazy loading of translation files
-
# * available_locales are determined by looking at translation file names
-
1
class Backend
-
3
(class << self; self; end).class_eval { public :include }
-
-
1
module Implementation
-
1
include ::I18n::Backend::Base
-
-
# Stores translations for the given locale in memory.
-
# This uses a deep merge for the translations hash, so existing
-
# translations will be overwritten by new ones only at the deepest
-
# level of the hash.
-
1
def store_translations(locale, data, options = {})
-
6
locale = locale.to_sym
-
6
translations[locale] ||= {}
-
6
data = data.deep_symbolize_keys
-
6
translations[locale].deep_merge!(data)
-
end
-
-
# Get available locales from the translations filenames
-
1
def available_locales
-
3589
@available_locales ||= ::I18n.load_path.map {|path| File.basename(path, '.*')}.uniq.sort.map(&:to_sym)
-
end
-
-
# Clean up translations
-
1
def reload!
-
1
@translations = nil
-
1
@available_locales = nil
-
1
super
-
end
-
-
1
protected
-
-
1
def init_translations(locale)
-
1
locale = locale.to_s
-
63
paths = ::I18n.load_path.select {|path| File.basename(path, '.*') == locale}
-
1
load_translations(paths)
-
1
translations[locale] ||= {}
-
end
-
-
1
def translations
-
83393
@translations ||= {}
-
end
-
-
# Looks up a translation from the translations hash. Returns nil if
-
# eiher key is nil, or locale, scope or key do not exist as a key in the
-
# nested translations hash. Splits keys or scopes containing dots
-
# into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
-
# <tt>%w(currency format)</tt>.
-
1
def lookup(locale, key, scope = [], options = {})
-
41690
init_translations(locale) unless translations.key?(locale)
-
41690
keys = ::I18n.normalize_keys(locale, key, scope, options[:separator])
-
-
41690
keys.inject(translations) do |result, _key|
-
86882
_key = _key.to_sym
-
86882
return nil unless result.is_a?(Hash) && result.has_key?(_key)
-
85614
result = result[_key]
-
85614
result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
-
85614
result
-
end
-
end
-
end
-
-
1
include Implementation
-
# Adds fallback to default locale for untranslated strings
-
1
include ::I18n::Backend::Fallbacks
-
end
-
end
-
end
-
1
module Redmine
-
1
module Info
-
1
class << self
-
749
def app_name; 'Redmine' end
-
375
def url; 'http://www.redmine.org/' end
-
2
def help_url; 'http://www.redmine.org/guide' end
-
1
def versioned_name; "#{app_name} #{Redmine::VERSION}" end
-
-
1
def environment
-
s = "Environment:\n"
-
s << [
-
["Redmine version", Redmine::VERSION],
-
["Ruby version", "#{RUBY_VERSION} (#{RUBY_PLATFORM})"],
-
["Rails version", Rails::VERSION::STRING],
-
["Environment", Rails.env],
-
["Database adapter", ActiveRecord::Base.connection.adapter_name]
-
].map {|info| " %-40s %s" % info}.join("\n")
-
s << "\nRedmine plugins:\n"
-
-
plugins = Redmine::Plugin.all
-
if plugins.any?
-
s << plugins.map {|plugin| " %-40s %s" % [plugin.id.to_s, plugin.version.to_s]}.join("\n")
-
else
-
s << " no plugin installed"
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module MenuManager
-
1
class MenuError < StandardError #:nodoc:
-
end
-
-
1
module MenuController
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
28
@@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
-
1
mattr_accessor :menu_items
-
-
# Set the menu item name for a controller or specific actions
-
# Examples:
-
# * menu_item :tickets # => sets the menu name to :tickets for the whole controller
-
# * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
-
# * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
-
#
-
# The default menu item name for a controller is controller_name by default
-
# Eg. the default menu item name for ProjectsController is :projects
-
1
def menu_item(id, options = {})
-
23
if actions = options[:only]
-
8
actions = [] << actions unless actions.is_a?(Array)
-
22
actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
-
else
-
15
menu_items[controller_name.to_sym][:default] = id
-
end
-
end
-
end
-
-
1
def menu_items
-
923
self.class.menu_items
-
end
-
-
# Returns the menu item name according to the current action
-
1
def current_menu_item
-
@current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
-
5311
menu_items[controller_name.to_sym][:default]
-
end
-
-
# Redirects user to the menu item of the given project
-
# Returns false if user is not authorized
-
1
def redirect_to_project_menu_item(project, name)
-
item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s}
-
if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project))
-
redirect_to({item.param => project}.merge(item.url))
-
return true
-
end
-
false
-
end
-
end
-
-
1
module MenuHelper
-
# Returns the current menu item name
-
1
def current_menu_item
-
5311
controller.current_menu_item
-
end
-
-
# Renders the application main menu
-
1
def render_main_menu(project)
-
374
render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
-
end
-
-
1
def display_main_menu?(project)
-
374
menu_name = project && !project.new_record? ? :project_menu : :application_menu
-
374
Redmine::MenuManager.items(menu_name).children.present?
-
end
-
-
1
def render_menu(menu, project=nil)
-
1125
links = []
-
1125
menu_items_for(menu, project) do |node|
-
4193
links << render_menu_node(node, project)
-
end
-
1125
links.empty? ? nil : content_tag('ul', links.join("\n").html_safe)
-
end
-
-
1
def render_menu_node(node, project=nil)
-
4193
if node.children.present? || !node.child_menus.nil?
-
return render_menu_node_with_children(node, project)
-
else
-
4193
caption, url, selected = extract_node_details(node, project)
-
4193
return content_tag('li',
-
render_single_menu_node(node, caption, url, selected))
-
end
-
end
-
-
1
def render_menu_node_with_children(node, project=nil)
-
caption, url, selected = extract_node_details(node, project)
-
-
html = [].tap do |html|
-
html << '<li>'
-
# Parent
-
html << render_single_menu_node(node, caption, url, selected)
-
-
# Standard children
-
standard_children_list = "".html_safe.tap do |child_html|
-
node.children.each do |child|
-
child_html << render_menu_node(child, project)
-
end
-
end
-
-
html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty?
-
-
# Unattached children
-
unattached_children_list = render_unattached_children_menu(node, project)
-
html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank?
-
-
html << '</li>'
-
end
-
return html.join("\n").html_safe
-
end
-
-
# Returns a list of unattached children menu items
-
1
def render_unattached_children_menu(node, project)
-
return nil unless node.child_menus
-
-
"".html_safe.tap do |child_html|
-
unattached_children = node.child_menus.call(project)
-
# Tree nodes support #each so we need to do object detection
-
if unattached_children.is_a? Array
-
unattached_children.each do |child|
-
child_html << content_tag(:li, render_unattached_menu_item(child, project))
-
end
-
else
-
raise MenuError, ":child_menus must be an array of MenuItems"
-
end
-
end
-
end
-
-
1
def render_single_menu_node(item, caption, url, selected)
-
4193
link_to(h(caption), url, item.html_options(:selected => selected))
-
end
-
-
1
def render_unattached_menu_item(menu_item, project)
-
raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem
-
-
if User.current.allowed_to?(menu_item.url, project)
-
link_to(h(menu_item.caption),
-
menu_item.url,
-
menu_item.html_options)
-
end
-
end
-
-
1
def menu_items_for(menu, project=nil)
-
1125
items = []
-
1125
Redmine::MenuManager.items(menu).root.children.each do |node|
-
6121
if allowed_node?(node, User.current, project)
-
4193
if block_given?
-
4193
yield node
-
else
-
items << node # TODO: not used?
-
end
-
end
-
end
-
1125
return block_given? ? nil : items
-
end
-
-
1
def extract_node_details(node, project=nil)
-
4193
item = node
-
4193
url = case item.url
-
when Hash
-
2946
project.nil? ? item.url : {item.param => project}.merge(item.url)
-
when Symbol
-
873
send(item.url)
-
else
-
374
item.url
-
end
-
4193
caption = item.caption(project)
-
4193
return [caption, url, (current_menu_item == item.name)]
-
end
-
-
# Checks if a user is allowed to access the menu item by:
-
#
-
# * Checking the conditions of the item
-
# * Checking the url target (project only)
-
1
def allowed_node?(node, user, project)
-
6121
if node.condition && !node.condition.call(project)
-
# Condition that doesn't pass
-
1910
return false
-
end
-
-
4211
if project
-
1949
return user && user.allowed_to?(node.url, project)
-
else
-
# outside a project, all menu items allowed
-
2262
return true
-
end
-
end
-
end
-
-
1
class << self
-
1
def map(menu_name)
-
10
@items ||= {}
-
10
mapper = Mapper.new(menu_name.to_sym, @items)
-
10
if block_given?
-
5
yield mapper
-
else
-
5
mapper
-
end
-
end
-
-
1
def items(menu_name)
-
1499
@items[menu_name.to_sym] || MenuNode.new(:root, {})
-
end
-
end
-
-
1
class Mapper
-
1
def initialize(menu, items)
-
10
items[menu] ||= MenuNode.new(:root, {})
-
10
@menu = menu
-
10
@menu_items = items[menu]
-
end
-
-
# Adds an item at the end of the menu. Available options:
-
# * param: the parameter name that is used for the project id (default is :id)
-
# * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
-
# * caption that can be:
-
# * a localized string Symbol
-
# * a String
-
# * a Proc that can take the project as argument
-
# * before, after: specify where the menu item should be inserted (eg. :after => :activity)
-
# * parent: menu item will be added as a child of another named menu (eg. :parent => :issues)
-
# * children: a Proc that is called before rendering the item. The Proc should return an array of MenuItems, which will be added as children to this item.
-
# eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] }
-
# * last: menu item will stay at the end (eg. :last => true)
-
# * html_options: a hash of html options that are passed to link_to
-
1
def push(name, url, options={})
-
41
options = options.dup
-
-
41
if options[:parent]
-
subtree = self.find(options[:parent])
-
if subtree
-
target_root = subtree
-
else
-
target_root = @menu_items.root
-
end
-
-
else
-
41
target_root = @menu_items.root
-
end
-
-
# menu item position
-
41
if first = options.delete(:first)
-
target_root.prepend(MenuItem.new(name, url, options))
-
41
elsif before = options.delete(:before)
-
-
if exists?(before)
-
target_root.add_at(MenuItem.new(name, url, options), position_of(before))
-
else
-
target_root.add(MenuItem.new(name, url, options))
-
end
-
-
41
elsif after = options.delete(:after)
-
-
3
if exists?(after)
-
3
target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1)
-
else
-
target_root.add(MenuItem.new(name, url, options))
-
end
-
-
38
elsif options[:last] # don't delete, needs to be stored
-
5
target_root.add_last(MenuItem.new(name, url, options))
-
else
-
33
target_root.add(MenuItem.new(name, url, options))
-
end
-
end
-
-
# Removes a menu item
-
1
def delete(name)
-
if found = self.find(name)
-
@menu_items.remove!(found)
-
end
-
end
-
-
# Checks if a menu item exists
-
1
def exists?(name)
-
18
@menu_items.any? {|node| node.name == name}
-
end
-
-
1
def find(name)
-
@menu_items.find {|node| node.name == name}
-
end
-
-
1
def position_of(name)
-
3
@menu_items.each do |node|
-
15
if node.name == name
-
3
return node.position
-
end
-
end
-
end
-
end
-
-
1
class MenuNode
-
1
include Enumerable
-
1
attr_accessor :parent
-
1
attr_reader :last_items_count, :name
-
-
1
def initialize(name, content = nil)
-
46
@name = name
-
46
@children = []
-
46
@last_items_count = 0
-
end
-
-
1
def children
-
5995
if block_given?
-
559
@children.each {|child| yield child}
-
else
-
5695
@children
-
end
-
end
-
-
# Returns the number of descendants + 1
-
1
def size
-
@children.inject(1) {|sum, node| sum + node.size}
-
end
-
-
1
def each &block
-
306
yield self
-
559
children { |child| child.each(&block) }
-
end
-
-
# Adds a child at first position
-
1
def prepend(child)
-
add_at(child, 0)
-
end
-
-
# Adds a child at given position
-
1
def add_at(child, position)
-
317
raise "Child already added" if find {|node| node.name == child.name}
-
-
41
@children = @children.insert(position, child)
-
41
child.parent = self
-
41
child
-
end
-
-
# Adds a child as last child
-
1
def add_last(child)
-
5
add_at(child, -1)
-
5
@last_items_count += 1
-
5
child
-
end
-
-
# Adds a child
-
1
def add(child)
-
33
position = @children.size - @last_items_count
-
33
add_at(child, position)
-
end
-
1
alias :<< :add
-
-
# Removes a child
-
1
def remove!(child)
-
@children.delete(child)
-
@last_items_count -= +1 if child && child.last
-
child.parent = nil
-
child
-
end
-
-
# Returns the position for this node in it's parent
-
1
def position
-
3
self.parent.children.index(self)
-
end
-
-
# Returns the root for this node
-
1
def root
-
1166
root = self
-
1166
root = root.parent while root.parent
-
1166
root
-
end
-
end
-
-
1
class MenuItem < MenuNode
-
1
include Redmine::I18n
-
1
attr_reader :name, :url, :param, :condition, :parent, :child_menus, :last
-
-
1
def initialize(name, url, options)
-
41
raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
-
41
raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
-
41
raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym
-
41
raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call)
-
41
@name = name
-
41
@url = url
-
41
@condition = options[:if]
-
41
@param = options[:param] || :id
-
41
@caption = options[:caption]
-
41
@html_options = options[:html] || {}
-
# Adds a unique class to each menu item based on its name
-
41
@html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
-
41
@parent = options[:parent]
-
41
@child_menus = options[:children]
-
41
@last = options[:last] || false
-
41
super @name.to_sym
-
end
-
-
1
def caption(project=nil)
-
4193
if @caption.is_a?(Proc)
-
c = @caption.call(project).to_s
-
c = @name.to_s.humanize if c.blank?
-
c
-
else
-
4193
if @caption.nil?
-
2478
l_or_humanize(name, :prefix => 'label_')
-
else
-
1715
@caption.is_a?(Symbol) ? l(@caption) : @caption
-
end
-
end
-
end
-
-
1
def html_options(options={})
-
4193
if options[:selected]
-
121
o = @html_options.dup
-
121
o[:class] += ' selected'
-
121
o
-
else
-
4072
@html_options
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module MimeType
-
-
1
MIME_TYPES = {
-
'text/plain' => 'txt,tpl,properties,patch,diff,ini,readme,install,upgrade',
-
'text/css' => 'css',
-
'text/html' => 'html,htm,xhtml',
-
'text/jsp' => 'jsp',
-
'text/x-c' => 'c,cpp,cc,h,hh',
-
'text/x-csharp' => 'cs',
-
'text/x-java' => 'java',
-
'text/x-html-template' => 'rhtml',
-
'text/x-perl' => 'pl,pm',
-
'text/x-php' => 'php,php3,php4,php5',
-
'text/x-python' => 'py',
-
'text/x-ruby' => 'rb,rbw,ruby,rake,erb',
-
'text/x-csh' => 'csh',
-
'text/x-sh' => 'sh',
-
'text/xml' => 'xml,xsd,mxml',
-
'text/yaml' => 'yml,yaml',
-
'text/csv' => 'csv',
-
'text/x-po' => 'po',
-
'image/gif' => 'gif',
-
'image/jpeg' => 'jpg,jpeg,jpe',
-
'image/png' => 'png',
-
'image/tiff' => 'tiff,tif',
-
'image/x-ms-bmp' => 'bmp',
-
'image/x-xpixmap' => 'xpm',
-
'image/svg+xml'=> 'svg',
-
'application/javascript' => 'js',
-
'application/pdf' => 'pdf',
-
'application/rtf' => 'rtf',
-
'application/msword' => 'doc',
-
'application/vnd.ms-excel' => 'xls',
-
'application/vnd.ms-powerpoint' => 'ppt,pps',
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
-
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
-
'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx',
-
'application/vnd.oasis.opendocument.spreadsheet' => 'ods',
-
'application/vnd.oasis.opendocument.text' => 'odt',
-
'application/vnd.oasis.opendocument.presentation' => 'odp',
-
'application/x-7z-compressed' => '7z',
-
'application/x-rar-compressed' => 'rar',
-
'application/x-tar' => 'tar',
-
'application/zip' => 'zip',
-
'application/x-gzip' => 'gz',
-
}.freeze
-
-
1
EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)|
-
115
exts.split(',').each {|ext| map[ext.strip] = type}
-
43
map
-
end
-
-
# returns mime type for name or nil if unknown
-
1
def self.of(name)
-
return nil unless name
-
m = name.to_s.match(/(^|\.)([^\.]+)$/)
-
EXTENSIONS[m[2].downcase] if m
-
end
-
-
# Returns the css class associated to
-
# the mime type of name
-
1
def self.css_class_of(name)
-
mime = of(name)
-
mime && mime.gsub('/', '-')
-
end
-
-
1
def self.main_mimetype_of(name)
-
mimetype = of(name)
-
mimetype.split('/').first if mimetype
-
end
-
-
# return true if mime-type for name is type/*
-
# otherwise false
-
1
def self.is_type?(type, name)
-
main_mimetype = main_mimetype_of(name)
-
type.to_s == main_mimetype
-
end
-
end
-
end
-
1
module Redmine
-
1
class Notifiable < Struct.new(:name, :parent)
-
-
1
def to_s
-
name
-
end
-
-
# TODO: Plugin API for adding a new notification?
-
1
def self.all
-
notifications = []
-
notifications << Notifiable.new('issue_added')
-
notifications << Notifiable.new('issue_updated')
-
notifications << Notifiable.new('issue_note_added', 'issue_updated')
-
notifications << Notifiable.new('issue_status_updated', 'issue_updated')
-
notifications << Notifiable.new('issue_priority_updated', 'issue_updated')
-
notifications << Notifiable.new('news_added')
-
notifications << Notifiable.new('news_comment_added')
-
notifications << Notifiable.new('document_added')
-
notifications << Notifiable.new('file_added')
-
notifications << Notifiable.new('message_posted')
-
notifications << Notifiable.new('wiki_content_added')
-
notifications << Notifiable.new('wiki_content_updated')
-
notifications
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine #:nodoc:
-
-
1
class PluginNotFound < StandardError; end
-
1
class PluginRequirementError < StandardError; end
-
-
# Base class for Redmine plugins.
-
# Plugins are registered using the <tt>register</tt> class method that acts as the public constructor.
-
#
-
# Redmine::Plugin.register :example do
-
# name 'Example plugin'
-
# author 'John Smith'
-
# description 'This is an example plugin for Redmine'
-
# version '0.0.1'
-
# settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
-
# end
-
#
-
# === Plugin attributes
-
#
-
# +settings+ is an optional attribute that let the plugin be configurable.
-
# It must be a hash with the following keys:
-
# * <tt>:default</tt>: default value for the plugin settings
-
# * <tt>:partial</tt>: path of the configuration partial view, relative to the plugin <tt>app/views</tt> directory
-
# Example:
-
# settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
-
# In this example, the settings partial will be found here in the plugin directory: <tt>app/views/settings/_settings.rhtml</tt>.
-
#
-
# When rendered, the plugin settings value is available as the local variable +settings+
-
1
class Plugin
-
1
cattr_accessor :directory
-
1
self.directory = File.join(Rails.root, 'plugins')
-
-
1
cattr_accessor :public_directory
-
1
self.public_directory = File.join(Rails.root, 'public', 'plugin_assets')
-
-
1
@registered_plugins = {}
-
1
class << self
-
1
attr_reader :registered_plugins
-
1
private :new
-
-
1
def def_field(*names)
-
1
class_eval do
-
1
names.each do |name|
-
7
define_method(name) do |*args|
-
4220
args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
-
end
-
end
-
end
-
end
-
end
-
1
def_field :name, :description, :url, :author, :author_url, :version, :settings
-
1
attr_reader :id
-
-
# Plugin constructor
-
1
def self.register(id, &block)
-
1
p = new(id)
-
1
p.instance_eval(&block)
-
# Set a default name if it was not provided during registration
-
1
p.name(id.to_s.humanize) if p.name.nil?
-
-
# Adds plugin locales if any
-
# YAML translation files should be found under <plugin>/config/locales/
-
1
::I18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
-
-
# Prepends the app/views directory of the plugin to the view path
-
1
view_path = File.join(p.directory, 'app', 'views')
-
1
if File.directory?(view_path)
-
1
ActionController::Base.prepend_view_path(view_path)
-
1
ActionMailer::Base.prepend_view_path(view_path)
-
end
-
-
# Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
-
1
Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
-
3
ActiveSupport::Dependencies.autoload_paths += [dir]
-
end
-
-
1
registered_plugins[id] = p
-
end
-
-
# Returns an array of all registered plugins
-
1
def self.all
-
3
registered_plugins.values.sort
-
end
-
-
# Finds a plugin by its id
-
# Returns a PluginNotFound exception if the plugin doesn't exist
-
1
def self.find(id)
-
4201
registered_plugins[id.to_sym] || raise(PluginNotFound)
-
end
-
-
# Clears the registered plugins hash
-
# It doesn't unload installed plugins
-
1
def self.clear
-
@registered_plugins = {}
-
end
-
-
# Checks if a plugin is installed
-
#
-
# @param [String] id name of the plugin
-
1
def self.installed?(id)
-
registered_plugins[id.to_sym].present?
-
end
-
-
1
def self.load
-
1
Dir.glob(File.join(self.directory, '*')).sort.each do |directory|
-
2
if File.directory?(directory)
-
1
lib = File.join(directory, "lib")
-
1
if File.directory?(lib)
-
1
$:.unshift lib
-
1
ActiveSupport::Dependencies.autoload_paths += [lib]
-
end
-
1
initializer = File.join(directory, "init.rb")
-
1
if File.file?(initializer)
-
1
require initializer
-
end
-
end
-
end
-
end
-
-
1
def initialize(id)
-
1
@id = id.to_sym
-
end
-
-
1
def directory
-
4
File.join(self.class.directory, id.to_s)
-
end
-
-
1
def public_directory
-
1
File.join(self.class.public_directory, id.to_s)
-
end
-
-
1
def assets_directory
-
1
File.join(directory, 'assets')
-
end
-
-
1
def <=>(plugin)
-
self.id.to_s <=> plugin.id.to_s
-
end
-
-
# Sets a requirement on Redmine version
-
# Raises a PluginRequirementError exception if the requirement is not met
-
#
-
# Examples
-
# # Requires Redmine 0.7.3 or higher
-
# requires_redmine :version_or_higher => '0.7.3'
-
# requires_redmine '0.7.3'
-
#
-
# # Requires Redmine 0.7.x or higher
-
# requires_redmine '0.7'
-
#
-
# # Requires a specific Redmine version
-
# requires_redmine :version => '0.7.3' # 0.7.3 only
-
# requires_redmine :version => '0.7' # 0.7.x
-
# requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
-
#
-
# # Requires a Redmine version within a range
-
# requires_redmine :version => '0.7.3'..'0.9.1' # >= 0.7.3 and <= 0.9.1
-
# requires_redmine :version => '0.7'..'0.9' # >= 0.7.x and <= 0.9.x
-
1
def requires_redmine(arg)
-
arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
-
arg.assert_valid_keys(:version, :version_or_higher)
-
-
current = Redmine::VERSION.to_a
-
arg.each do |k, req|
-
case k
-
when :version_or_higher
-
raise ArgumentError.new(":version_or_higher accepts a version string only") unless req.is_a?(String)
-
unless compare_versions(req, current) <= 0
-
raise PluginRequirementError.new("#{id} plugin requires Redmine #{req} or higher but current is #{current.join('.')}")
-
end
-
when :version
-
req = [req] if req.is_a?(String)
-
if req.is_a?(Array)
-
unless req.detect {|ver| compare_versions(ver, current) == 0}
-
raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{req.join(', ')} but current is #{current.join('.')}")
-
end
-
elsif req.is_a?(Range)
-
unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0
-
raise PluginRequirementError.new("#{id} plugin requires a Redmine version between #{req.first} and #{req.last} but current is #{current.join('.')}")
-
end
-
else
-
raise ArgumentError.new(":version option accepts a version string, an array or a range of versions")
-
end
-
end
-
end
-
true
-
end
-
-
1
def compare_versions(requirement, current)
-
requirement = requirement.split('.').collect(&:to_i)
-
requirement <=> current.slice(0, requirement.size)
-
end
-
1
private :compare_versions
-
-
# Sets a requirement on a Redmine plugin version
-
# Raises a PluginRequirementError exception if the requirement is not met
-
#
-
# Examples
-
# # Requires a plugin named :foo version 0.7.3 or higher
-
# requires_redmine_plugin :foo, :version_or_higher => '0.7.3'
-
# requires_redmine_plugin :foo, '0.7.3'
-
#
-
# # Requires a specific version of a Redmine plugin
-
# requires_redmine_plugin :foo, :version => '0.7.3' # 0.7.3 only
-
# requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
-
1
def requires_redmine_plugin(plugin_name, arg)
-
arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
-
arg.assert_valid_keys(:version, :version_or_higher)
-
-
plugin = Plugin.find(plugin_name)
-
current = plugin.version.split('.').collect(&:to_i)
-
-
arg.each do |k, v|
-
v = [] << v unless v.is_a?(Array)
-
versions = v.collect {|s| s.split('.').collect(&:to_i)}
-
case k
-
when :version_or_higher
-
raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
-
unless (current <=> versions.first) >= 0
-
raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin #{v} or higher but current is #{current.join('.')}")
-
end
-
when :version
-
unless versions.include?(current.slice(0,3))
-
raise PluginRequirementError.new("#{id} plugin requires one the following versions of #{plugin_name}: #{v.join(', ')} but current is #{current.join('.')}")
-
end
-
end
-
end
-
true
-
end
-
-
# Adds an item to the given +menu+.
-
# The +id+ parameter (equals to the project id) is automatically added to the url.
-
# menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
-
#
-
# +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu
-
#
-
1
def menu(menu, item, url, options={})
-
5
Redmine::MenuManager.map(menu).push(item, url, options)
-
end
-
1
alias :add_menu_item :menu
-
-
# Removes +item+ from the given +menu+.
-
1
def delete_menu_item(menu, item)
-
Redmine::MenuManager.map(menu).delete(item)
-
end
-
-
# Defines a permission called +name+ for the given +actions+.
-
#
-
# The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array):
-
# permission :destroy_contacts, { :contacts => :destroy }
-
# permission :view_contacts, { :contacts => [:index, :show] }
-
#
-
# The +options+ argument is a hash that accept the following keys:
-
# * :public => the permission is public if set to true (implicitly given to any user)
-
# * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member
-
# * :read => set it to true so that the permission is still granted on closed projects
-
#
-
# Examples
-
# # A permission that is implicitly given to any user
-
# # This permission won't appear on the Roles & Permissions setup screen
-
# permission :say_hello, { :example => :say_hello }, :public => true, :read => true
-
#
-
# # A permission that can be given to any user
-
# permission :say_hello, { :example => :say_hello }
-
#
-
# # A permission that can be given to registered users only
-
# permission :say_hello, { :example => :say_hello }, :require => :loggedin
-
#
-
# # A permission that can be given to project members only
-
# permission :say_hello, { :example => :say_hello }, :require => :member
-
1
def permission(name, actions, options = {})
-
17
if @project_module
-
51
Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
-
else
-
Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
-
end
-
end
-
-
# Defines a project module, that can be enabled/disabled for each project.
-
# Permissions defined inside +block+ will be bind to the module.
-
#
-
# project_module :things do
-
# permission :view_contacts, { :contacts => [:list, :show] }, :public => true
-
# permission :destroy_contacts, { :contacts => :destroy }
-
# end
-
1
def project_module(name, &block)
-
1
@project_module = name
-
1
self.instance_eval(&block)
-
1
@project_module = nil
-
end
-
-
# Registers an activity provider.
-
#
-
# Options:
-
# * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
-
# * <tt>:default</tt> - setting this option to false will make the events not displayed by default
-
#
-
# A model can provide several activity event types.
-
#
-
# Examples:
-
# register :news
-
# register :scrums, :class_name => 'Meeting'
-
# register :issues, :class_name => ['Issue', 'Journal']
-
#
-
# Retrieving events:
-
# Associated model(s) must implement the find_events class method.
-
# ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
-
#
-
# The following call should return all the scrum events visible by current user that occured in the 5 last days:
-
# Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
-
# Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
-
#
-
# Note that :view_scrums permission is required to view these events in the activity view.
-
1
def activity_provider(*args)
-
Redmine::Activity.register(*args)
-
end
-
-
# Registers a wiki formatter.
-
#
-
# Parameters:
-
# * +name+ - human-readable name
-
# * +formatter+ - formatter class, which should have an instance method +to_html+
-
# * +helper+ - helper module, which will be included by wiki pages
-
1
def wiki_format_provider(name, formatter, helper)
-
Redmine::WikiFormatting.register(name, formatter, helper)
-
end
-
-
# Returns +true+ if the plugin can be configured.
-
1
def configurable?
-
1
settings && settings.is_a?(Hash) && !settings[:partial].blank?
-
end
-
-
1
def mirror_assets
-
1
source = assets_directory
-
1
destination = public_directory
-
1
return unless File.directory?(source)
-
-
1
source_files = Dir[source + "/**/*"]
-
486
source_dirs = source_files.select { |d| File.directory?(d) }
-
1
source_files -= source_dirs
-
-
1
unless source_files.empty?
-
1
base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
-
1
begin
-
1
FileUtils.mkdir_p(base_target_dir)
-
rescue Exception => e
-
raise "Could not create directory #{base_target_dir}: " + e.message
-
end
-
end
-
-
1
source_dirs.each do |dir|
-
# strip down these paths so we have simple, relative paths we can
-
# add to the destination
-
30
target_dir = File.join(destination, dir.gsub(source, ''))
-
30
begin
-
30
FileUtils.mkdir_p(target_dir)
-
rescue Exception => e
-
raise "Could not create directory #{target_dir}: " + e.message
-
end
-
end
-
-
1
source_files.each do |file|
-
455
begin
-
455
target = File.join(destination, file.gsub(source, ''))
-
455
unless File.exist?(target) && FileUtils.identical?(file, target)
-
FileUtils.cp(file, target)
-
end
-
rescue Exception => e
-
raise "Could not copy #{file} to #{target}: " + e.message
-
end
-
end
-
end
-
-
# Mirrors assets from one or all plugins to public/plugin_assets
-
1
def self.mirror_assets(name=nil)
-
1
if name.present?
-
find(name).mirror_assets
-
else
-
1
all.each do |plugin|
-
1
plugin.mirror_assets
-
end
-
end
-
end
-
-
# The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
-
1
def migration_directory
-
File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate')
-
end
-
-
# Returns the version number of the latest migration for this plugin. Returns
-
# nil if this plugin has no migrations.
-
1
def latest_migration
-
migrations.last
-
end
-
-
# Returns the version numbers of all migrations for this plugin.
-
1
def migrations
-
migrations = Dir[migration_directory+"/*.rb"]
-
migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort
-
end
-
-
# Migrate this plugin to the given version
-
1
def migrate(version = nil)
-
puts "Migrating #{id} (#{name})..."
-
Redmine::Plugin::Migrator.migrate_plugin(self, version)
-
end
-
-
# Migrates all plugins or a single plugin to a given version
-
# Exemples:
-
# Plugin.migrate
-
# Plugin.migrate('sample_plugin')
-
# Plugin.migrate('sample_plugin', 1)
-
#
-
1
def self.migrate(name=nil, version=nil)
-
if name.present?
-
find(name).migrate(version)
-
else
-
all.each do |plugin|
-
plugin.migrate
-
end
-
end
-
end
-
-
1
class Migrator < ActiveRecord::Migrator
-
# We need to be able to set the 'current' plugin being migrated.
-
1
cattr_accessor :current_plugin
-
-
1
class << self
-
# Runs the migrations from a plugin, up (or down) to the version given
-
1
def migrate_plugin(plugin, version)
-
self.current_plugin = plugin
-
return if current_version(plugin) == version
-
migrate(plugin.migration_directory, version)
-
end
-
-
1
def current_version(plugin=current_plugin)
-
# Delete migrations that don't match .. to_i will work because the number comes first
-
::ActiveRecord::Base.connection.select_values(
-
"SELECT version FROM #{schema_migrations_table_name}"
-
).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0
-
end
-
end
-
-
1
def migrated
-
sm_table = self.class.schema_migrations_table_name
-
::ActiveRecord::Base.connection.select_values(
-
"SELECT version FROM #{sm_table}"
-
).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort
-
end
-
-
1
def record_version_state_after_migrating(version)
-
super(version.to_s + "-" + current_plugin.id.to_s)
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module SafeAttributes
-
1
def self.included(base)
-
15
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
# Declares safe attributes
-
# An optional Proc can be given for conditional inclusion
-
#
-
# Example:
-
# safe_attributes 'title', 'pages'
-
# safe_attributes 'isbn', :if => {|book, user| book.author == user}
-
1
def safe_attributes(*args)
-
272
@safe_attributes ||= []
-
272
if args.empty?
-
242
if superclass.include?(Redmine::SafeAttributes)
-
@safe_attributes + superclass.safe_attributes
-
else
-
242
@safe_attributes
-
end
-
else
-
30
options = args.last.is_a?(Hash) ? args.pop : {}
-
30
@safe_attributes << [args, options]
-
end
-
end
-
end
-
-
# Returns an array that can be safely set by user or current user
-
#
-
# Example:
-
# book.safe_attributes # => ['title', 'pages']
-
# book.safe_attributes(book.author) # => ['title', 'pages', 'isbn']
-
1
def safe_attribute_names(user=nil)
-
331
return @safe_attribute_names if @safe_attribute_names && user.nil?
-
242
names = []
-
242
self.class.safe_attributes.collect do |attrs, options|
-
2384
if options[:if].nil? || options[:if].call(self, user || User.current)
-
1652
names += attrs.collect(&:to_s)
-
end
-
end
-
242
names.uniq!
-
242
@safe_attribute_names = names if user.nil?
-
242
names
-
end
-
-
# Returns true if attr can be set by user or the current user
-
1
def safe_attribute?(attr, user=nil)
-
96
safe_attribute_names(user).include?(attr.to_s)
-
end
-
-
# Returns a hash with unsafe attributes removed
-
# from the given attrs hash
-
#
-
# Example:
-
# book.delete_unsafe_attributes({'title' => 'My book', 'foo' => 'bar'})
-
# # => {'title' => 'My book'}
-
1
def delete_unsafe_attributes(attrs, user=User.current)
-
4
safe = safe_attribute_names(user)
-
44
attrs.dup.delete_if {|k,v| !safe.include?(k)}
-
end
-
-
# Sets attributes from attrs that are safe
-
# attrs is a Hash with string keys
-
1
def safe_attributes=(attrs, user=User.current)
-
4
return unless attrs.is_a?(Hash)
-
2
self.attributes = delete_unsafe_attributes(attrs, user)
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'cgi'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class CommandFailed < StandardError #:nodoc:
-
end
-
-
1
class AbstractAdapter #:nodoc:
-
-
# raised if scm command exited with error, e.g. unknown revision.
-
1
class ScmCommandAborted < CommandFailed; end
-
-
1
class << self
-
1
def client_command
-
""
-
end
-
-
1
def shell_quote_command
-
if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
-
client_command
-
else
-
shell_quote(client_command)
-
end
-
end
-
-
# Returns the version of the scm client
-
# Eg: [1, 5, 0] or [] if unknown
-
1
def client_version
-
[]
-
end
-
-
# Returns the version string of the scm client
-
# Eg: '1.5.0' or 'Unknown version' if unknown
-
1
def client_version_string
-
v = client_version || 'Unknown version'
-
v.is_a?(Array) ? v.join('.') : v.to_s
-
end
-
-
# Returns true if the current client version is above
-
# or equals the given one
-
# If option is :unknown is set to true, it will return
-
# true if the client version is unknown
-
1
def client_version_above?(v, options={})
-
((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
-
end
-
-
1
def client_available
-
true
-
end
-
-
1
def shell_quote(str)
-
if Redmine::Platform.mswin?
-
'"' + str.gsub(/"/, '\\"') + '"'
-
else
-
"'" + str.gsub(/'/, "'\"'\"'") + "'"
-
end
-
end
-
end
-
-
1
def initialize(url, root_url=nil, login=nil, password=nil,
-
path_encoding=nil)
-
@url = url
-
@login = login if login && !login.empty?
-
@password = (password || "") if @login
-
@root_url = root_url.blank? ? retrieve_root_url : root_url
-
end
-
-
1
def adapter_name
-
'Abstract'
-
end
-
-
1
def supports_cat?
-
true
-
end
-
-
1
def supports_annotate?
-
respond_to?('annotate')
-
end
-
-
1
def root_url
-
@root_url
-
end
-
-
1
def url
-
@url
-
end
-
-
1
def path_encoding
-
nil
-
end
-
-
# get info about the svn repository
-
1
def info
-
return nil
-
end
-
-
# Returns the entry identified by path and revision identifier
-
# or nil if entry doesn't exist in the repository
-
1
def entry(path=nil, identifier=nil)
-
parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
-
search_path = parts[0..-2].join('/')
-
search_name = parts[-1]
-
if search_path.blank? && search_name.blank?
-
# Root entry
-
Entry.new(:path => '', :kind => 'dir')
-
else
-
# Search for the entry in the parent directory
-
es = entries(search_path, identifier)
-
es ? es.detect {|e| e.name == search_name} : nil
-
end
-
end
-
-
# Returns an Entries collection
-
# or nil if the given path doesn't exist in the repository
-
1
def entries(path=nil, identifier=nil, options={})
-
return nil
-
end
-
-
1
def branches
-
return nil
-
end
-
-
1
def tags
-
return nil
-
end
-
-
1
def default_branch
-
return nil
-
end
-
-
1
def properties(path, identifier=nil)
-
return nil
-
end
-
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
return nil
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
return nil
-
end
-
-
1
def cat(path, identifier=nil)
-
return nil
-
end
-
-
1
def with_leading_slash(path)
-
path ||= ''
-
(path[0,1]!="/") ? "/#{path}" : path
-
end
-
-
1
def with_trailling_slash(path)
-
path ||= ''
-
(path[-1,1] == "/") ? path : "#{path}/"
-
end
-
-
1
def without_leading_slash(path)
-
path ||= ''
-
path.gsub(%r{^/+}, '')
-
end
-
-
1
def without_trailling_slash(path)
-
path ||= ''
-
(path[-1,1] == "/") ? path[0..-2] : path
-
end
-
-
1
def shell_quote(str)
-
self.class.shell_quote(str)
-
end
-
-
1
private
-
1
def retrieve_root_url
-
info = self.info
-
info ? info.root_url : nil
-
end
-
-
1
def target(path, sq=true)
-
path ||= ''
-
base = path.match(/^\//) ? root_url : url
-
str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
-
if sq
-
str = shell_quote(str)
-
end
-
str
-
end
-
-
1
def logger
-
self.class.logger
-
end
-
-
1
def shellout(cmd, options = {}, &block)
-
self.class.shellout(cmd, options, &block)
-
end
-
-
1
def self.logger
-
Rails.logger
-
end
-
-
1
def self.shellout(cmd, options = {}, &block)
-
if logger && logger.debug?
-
logger.debug "Shelling out: #{strip_credential(cmd)}"
-
end
-
if Rails.env == 'development'
-
# Capture stderr when running in dev environment
-
cmd = "#{cmd} 2>>#{shell_quote(Rails.root.join('log/scm.stderr.log').to_s)}"
-
end
-
begin
-
mode = "r+"
-
IO.popen(cmd, mode) do |io|
-
io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
-
io.close_write unless options[:write_stdin]
-
block.call(io) if block_given?
-
end
-
## If scm command does not exist,
-
## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
-
## in production environment.
-
# rescue Errno::ENOENT => e
-
rescue Exception => e
-
msg = strip_credential(e.message)
-
# The command failed, log it and re-raise
-
logmsg = "SCM command failed, "
-
logmsg += "make sure that your SCM command (e.g. svn) is "
-
logmsg += "in PATH (#{ENV['PATH']})\n"
-
logmsg += "You can configure your scm commands in config/configuration.yml.\n"
-
logmsg += "#{strip_credential(cmd)}\n"
-
logmsg += "with: #{msg}"
-
logger.error(logmsg)
-
raise CommandFailed.new(msg)
-
end
-
end
-
-
# Hides username/password in a given command
-
1
def self.strip_credential(cmd)
-
q = (Redmine::Platform.mswin? ? '"' : "'")
-
cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
-
end
-
-
1
def strip_credential(cmd)
-
self.class.strip_credential(cmd)
-
end
-
-
1
def scm_iconv(to, from, str)
-
return nil if str.nil?
-
return str if to == from
-
if str.respond_to?(:force_encoding)
-
str.force_encoding(from)
-
begin
-
str.encode(to)
-
rescue Exception => err
-
logger.error("failed to convert from #{from} to #{to}. #{err}")
-
nil
-
end
-
else
-
begin
-
Iconv.conv(to, from, str)
-
rescue Iconv::Failure => err
-
logger.error("failed to convert from #{from} to #{to}. #{err}")
-
nil
-
end
-
end
-
end
-
-
1
def parse_xml(xml)
-
if RUBY_PLATFORM == 'java'
-
xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
-
end
-
ActiveSupport::XmlMini.parse(xml)
-
end
-
end
-
-
1
class Entries < Array
-
1
def sort_by_name
-
dup.sort! {|x,y|
-
if x.kind == y.kind
-
x.name.to_s <=> y.name.to_s
-
else
-
x.kind <=> y.kind
-
end
-
}
-
end
-
-
1
def revisions
-
revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
-
end
-
end
-
-
1
class Info
-
1
attr_accessor :root_url, :lastrev
-
1
def initialize(attributes={})
-
self.root_url = attributes[:root_url] if attributes[:root_url]
-
self.lastrev = attributes[:lastrev]
-
end
-
end
-
-
1
class Entry
-
1
attr_accessor :name, :path, :kind, :size, :lastrev, :changeset
-
-
1
def initialize(attributes={})
-
self.name = attributes[:name] if attributes[:name]
-
self.path = attributes[:path] if attributes[:path]
-
self.kind = attributes[:kind] if attributes[:kind]
-
self.size = attributes[:size].to_i if attributes[:size]
-
self.lastrev = attributes[:lastrev]
-
end
-
-
1
def is_file?
-
'file' == self.kind
-
end
-
-
1
def is_dir?
-
'dir' == self.kind
-
end
-
-
1
def is_text?
-
Redmine::MimeType.is_type?('text', name)
-
end
-
-
1
def author
-
if changeset
-
changeset.author.to_s
-
elsif lastrev
-
Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first)
-
end
-
end
-
end
-
-
1
class Revisions < Array
-
1
def latest
-
sort {|x,y|
-
unless x.time.nil? or y.time.nil?
-
x.time <=> y.time
-
else
-
0
-
end
-
}.last
-
end
-
end
-
-
1
class Revision
-
1
attr_accessor :scmid, :name, :author, :time, :message,
-
:paths, :revision, :branch, :identifier,
-
:parents
-
-
1
def initialize(attributes={})
-
self.identifier = attributes[:identifier]
-
self.scmid = attributes[:scmid]
-
self.name = attributes[:name] || self.identifier
-
self.author = attributes[:author]
-
self.time = attributes[:time]
-
self.message = attributes[:message] || ""
-
self.paths = attributes[:paths]
-
self.revision = attributes[:revision]
-
self.branch = attributes[:branch]
-
self.parents = attributes[:parents]
-
end
-
-
# Returns the readable identifier.
-
1
def format_identifier
-
self.identifier.to_s
-
end
-
-
1
def ==(other)
-
if other.nil?
-
false
-
elsif scmid.present?
-
scmid == other.scmid
-
elsif identifier.present?
-
identifier == other.identifier
-
elsif revision.present?
-
revision == other.revision
-
end
-
end
-
end
-
-
1
class Annotate
-
1
attr_reader :lines, :revisions
-
-
1
def initialize
-
@lines = []
-
@revisions = []
-
end
-
-
1
def add_line(line, revision)
-
@lines << line
-
@revisions << revision
-
end
-
-
1
def content
-
content = lines.join("\n")
-
end
-
-
1
def empty?
-
lines.empty?
-
end
-
end
-
-
1
class Branch < String
-
1
attr_accessor :revision, :scmid
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class BazaarAdapter < AbstractAdapter
-
-
# Bazaar executable name
-
1
BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr"
-
-
1
class << self
-
1
def client_command
-
@@bin ||= BZR_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (scm_command_version || [])
-
end
-
-
1
def client_available
-
!client_version.empty?
-
end
-
-
1
def scm_command_version
-
scm_version = scm_version_from_command_line.dup
-
if scm_version.respond_to?(:force_encoding)
-
scm_version.force_encoding('ASCII-8BIT')
-
end
-
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def scm_version_from_command_line
-
shellout("#{sq_bin} --version") { |io| io.read }.to_s
-
end
-
end
-
-
1
def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
-
@url = url
-
@root_url = url
-
@path_encoding = 'UTF-8'
-
# do not call *super* for non ASCII repository path
-
end
-
-
1
def bzr_path_encodig=(encoding)
-
@path_encoding = encoding
-
end
-
-
# Get info about the repository
-
1
def info
-
cmd_args = %w|revno|
-
cmd_args << bzr_target('')
-
info = nil
-
scm_cmd(*cmd_args) do |io|
-
if io.read =~ %r{^(\d+)\r?$}
-
info = Info.new({:root_url => url,
-
:lastrev => Revision.new({
-
:identifier => $1
-
})
-
})
-
end
-
end
-
info
-
rescue ScmCommandAborted
-
return nil
-
end
-
-
# Returns an Entries collection
-
# or nil if the given path doesn't exist in the repository
-
1
def entries(path=nil, identifier=nil, options={})
-
path ||= ''
-
entries = Entries.new
-
identifier = -1 unless identifier && identifier.to_i > 0
-
cmd_args = %w|ls -v --show-ids|
-
cmd_args << "-r#{identifier.to_i}"
-
cmd_args << bzr_target(path)
-
scm_cmd(*cmd_args) do |io|
-
prefix_utf8 = "#{url}/#{path}".gsub('\\', '/')
-
logger.debug "PREFIX: #{prefix_utf8}"
-
prefix = scm_iconv(@path_encoding, 'UTF-8', prefix_utf8)
-
prefix.force_encoding('ASCII-8BIT') if prefix.respond_to?(:force_encoding)
-
re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
-
io.each_line do |line|
-
next unless line =~ re
-
name_locale = $3.strip
-
name = scm_iconv('UTF-8', @path_encoding, name_locale)
-
entries << Entry.new({:name => name,
-
:path => ((path.empty? ? "" : "#{path}/") + name),
-
:kind => ($4.blank? ? 'file' : 'dir'),
-
:size => nil,
-
:lastrev => Revision.new(:revision => $5.strip)
-
})
-
end
-
end
-
if logger && logger.debug?
-
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}")
-
end
-
entries.sort_by_name
-
rescue ScmCommandAborted
-
return nil
-
end
-
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
path ||= ''
-
identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
-
identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
-
revisions = Revisions.new
-
cmd_args = %w|log -v --show-ids|
-
cmd_args << "-r#{identifier_to}..#{identifier_from}"
-
cmd_args << bzr_target(path)
-
scm_cmd(*cmd_args) do |io|
-
revision = nil
-
parsing = nil
-
io.each_line do |line|
-
if line =~ /^----/
-
revisions << revision if revision
-
revision = Revision.new(:paths => [], :message => '')
-
parsing = nil
-
else
-
next unless revision
-
if line =~ /^revno: (\d+)($|\s\[merge\]$)/
-
revision.identifier = $1.to_i
-
elsif line =~ /^committer: (.+)$/
-
revision.author = $1.strip
-
elsif line =~ /^revision-id:(.+)$/
-
revision.scmid = $1.strip
-
elsif line =~ /^timestamp: (.+)$/
-
revision.time = Time.parse($1).localtime
-
elsif line =~ /^ -----/
-
# partial revisions
-
parsing = nil unless parsing == 'message'
-
elsif line =~ /^(message|added|modified|removed|renamed):/
-
parsing = $1
-
elsif line =~ /^ (.*)$/
-
if parsing == 'message'
-
revision.message << "#{$1}\n"
-
else
-
if $1 =~ /^(.*)\s+(\S+)$/
-
path_locale = $1.strip
-
path = scm_iconv('UTF-8', @path_encoding, path_locale)
-
revid = $2
-
case parsing
-
when 'added'
-
revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
-
when 'modified'
-
revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
-
when 'removed'
-
revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
-
when 'renamed'
-
new_path = path.split('=>').last
-
if new_path
-
revision.paths << {:action => 'M', :path => "/#{new_path.strip}",
-
:revision => revid}
-
end
-
end
-
end
-
end
-
else
-
parsing = nil
-
end
-
end
-
end
-
revisions << revision if revision
-
end
-
revisions
-
rescue ScmCommandAborted
-
return nil
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
path ||= ''
-
if identifier_to
-
identifier_to = identifier_to.to_i
-
else
-
identifier_to = identifier_from.to_i - 1
-
end
-
if identifier_from
-
identifier_from = identifier_from.to_i
-
end
-
diff = []
-
cmd_args = %w|diff|
-
cmd_args << "-r#{identifier_to}..#{identifier_from}"
-
cmd_args << bzr_target(path)
-
scm_cmd_no_raise(*cmd_args) do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
diff
-
end
-
-
1
def cat(path, identifier=nil)
-
cat = nil
-
cmd_args = %w|cat|
-
cmd_args << "-r#{identifier.to_i}" if identifier && identifier.to_i > 0
-
cmd_args << bzr_target(path)
-
scm_cmd(*cmd_args) do |io|
-
io.binmode
-
cat = io.read
-
end
-
cat
-
rescue ScmCommandAborted
-
return nil
-
end
-
-
1
def annotate(path, identifier=nil)
-
blame = Annotate.new
-
cmd_args = %w|annotate -q --all|
-
cmd_args << "-r#{identifier.to_i}" if identifier && identifier.to_i > 0
-
cmd_args << bzr_target(path)
-
scm_cmd(*cmd_args) do |io|
-
author = nil
-
identifier = nil
-
io.each_line do |line|
-
next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
-
rev = $1
-
blame.add_line($3.rstrip,
-
Revision.new(
-
:identifier => rev,
-
:revision => rev,
-
:author => $2.strip
-
))
-
end
-
end
-
blame
-
rescue ScmCommandAborted
-
return nil
-
end
-
-
1
def self.branch_conf_path(path)
-
bcp = nil
-
m = path.match(%r{^(.*[/\\])\.bzr.*$})
-
if m
-
bcp = m[1]
-
else
-
bcp = path
-
end
-
bcp.gsub!(%r{[\/\\]$}, "")
-
if bcp
-
bcp = File.join(bcp, ".bzr", "branch", "branch.conf")
-
end
-
bcp
-
end
-
-
1
def append_revisions_only
-
return @aro if ! @aro.nil?
-
@aro = false
-
bcp = self.class.branch_conf_path(url)
-
if bcp && File.exist?(bcp)
-
begin
-
f = File::open(bcp, "r")
-
cnt = 0
-
f.each_line do |line|
-
l = line.chomp.to_s
-
if l =~ /^\s*append_revisions_only\s*=\s*(\w+)\s*$/
-
str_aro = $1
-
if str_aro.upcase == "TRUE"
-
@aro = true
-
cnt += 1
-
elsif str_aro.upcase == "FALSE"
-
@aro = false
-
cnt += 1
-
end
-
if cnt > 1
-
@aro = false
-
break
-
end
-
end
-
end
-
ensure
-
f.close
-
end
-
end
-
@aro
-
end
-
-
1
def scm_cmd(*args, &block)
-
full_args = []
-
full_args += args
-
full_args_locale = []
-
full_args.map do |e|
-
full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
-
end
-
ret = shellout(
-
self.class.sq_bin + ' ' +
-
full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
-
&block
-
)
-
if $? && $?.exitstatus != 0
-
raise ScmCommandAborted, "bzr exited with non-zero status: #{$?.exitstatus}"
-
end
-
ret
-
end
-
1
private :scm_cmd
-
-
1
def scm_cmd_no_raise(*args, &block)
-
full_args = []
-
full_args += args
-
full_args_locale = []
-
full_args.map do |e|
-
full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
-
end
-
ret = shellout(
-
self.class.sq_bin + ' ' +
-
full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
-
&block
-
)
-
ret
-
end
-
1
private :scm_cmd_no_raise
-
-
1
def bzr_target(path)
-
target(path, false)
-
end
-
1
private :bzr_target
-
end
-
end
-
end
-
end
-
# redMine - project management software
-
# Copyright (C) 2006-2007 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class CvsAdapter < AbstractAdapter
-
-
# CVS executable name
-
1
CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
-
-
1
class << self
-
1
def client_command
-
@@bin ||= CVS_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (scm_command_version || [])
-
end
-
-
1
def client_available
-
client_version_above?([1, 12])
-
end
-
-
1
def scm_command_version
-
scm_version = scm_version_from_command_line.dup
-
if scm_version.respond_to?(:force_encoding)
-
scm_version.force_encoding('ASCII-8BIT')
-
end
-
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def scm_version_from_command_line
-
shellout("#{sq_bin} --version") { |io| io.read }.to_s
-
end
-
end
-
-
# Guidelines for the input:
-
# url -> the project-path, relative to the cvsroot (eg. module name)
-
# root_url -> the good old, sometimes damned, CVSROOT
-
# login -> unnecessary
-
# password -> unnecessary too
-
1
def initialize(url, root_url=nil, login=nil, password=nil,
-
path_encoding=nil)
-
@path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
-
@url = url
-
# TODO: better Exception here (IllegalArgumentException)
-
raise CommandFailed if root_url.blank?
-
@root_url = root_url
-
-
# These are unused.
-
@login = login if login && !login.empty?
-
@password = (password || "") if @login
-
end
-
-
1
def path_encoding
-
@path_encoding
-
end
-
-
1
def info
-
logger.debug "<cvs> info"
-
Info.new({:root_url => @root_url, :lastrev => nil})
-
end
-
-
1
def get_previous_revision(revision)
-
CvsRevisionHelper.new(revision).prevRev
-
end
-
-
# Returns an Entries collection
-
# or nil if the given path doesn't exist in the repository
-
# this method is used by the repository-browser (aka LIST)
-
1
def entries(path=nil, identifier=nil, options={})
-
logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
-
path_locale = scm_iconv(@path_encoding, 'UTF-8', path)
-
path_locale.force_encoding("ASCII-8BIT") if path_locale.respond_to?(:force_encoding)
-
entries = Entries.new
-
cmd_args = %w|-q rls -e|
-
cmd_args << "-D" << time_to_cvstime_rlog(identifier) if identifier
-
cmd_args << path_with_proj(path)
-
scm_cmd(*cmd_args) do |io|
-
io.each_line() do |line|
-
fields = line.chop.split('/',-1)
-
logger.debug(">>InspectLine #{fields.inspect}")
-
if fields[0]!="D"
-
time = nil
-
# Thu Dec 13 16:27:22 2007
-
time_l = fields[-3].split(' ')
-
if time_l.size == 5 && time_l[4].length == 4
-
begin
-
time = Time.parse(
-
"#{time_l[1]} #{time_l[2]} #{time_l[3]} GMT #{time_l[4]}")
-
rescue
-
end
-
end
-
entries << Entry.new(
-
{
-
:name => scm_iconv('UTF-8', @path_encoding, fields[-5]),
-
#:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
-
:path => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[-5]}"),
-
:kind => 'file',
-
:size => nil,
-
:lastrev => Revision.new(
-
{
-
:revision => fields[-4],
-
:name => scm_iconv('UTF-8', @path_encoding, fields[-4]),
-
:time => time,
-
:author => ''
-
})
-
})
-
else
-
entries << Entry.new(
-
{
-
:name => scm_iconv('UTF-8', @path_encoding, fields[1]),
-
:path => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[1]}"),
-
:kind => 'dir',
-
:size => nil,
-
:lastrev => nil
-
})
-
end
-
end
-
end
-
entries.sort_by_name
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
STARTLOG="----------------------------"
-
1
ENDLOG ="============================================================================="
-
-
# Returns all revisions found between identifier_from and identifier_to
-
# in the repository. both identifier have to be dates or nil.
-
# these method returns nothing but yield every result in block
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
-
path_with_project_utf8 = path_with_proj(path)
-
path_with_project_locale = scm_iconv(@path_encoding, 'UTF-8', path_with_project_utf8)
-
logger.debug "<cvs> revisions path:" +
-
"'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
-
cmd_args = %w|-q rlog|
-
cmd_args << "-d" << ">#{time_to_cvstime_rlog(identifier_from)}" if identifier_from
-
cmd_args << path_with_project_utf8
-
scm_cmd(*cmd_args) do |io|
-
state = "entry_start"
-
commit_log = String.new
-
revision = nil
-
date = nil
-
author = nil
-
entry_path = nil
-
entry_name = nil
-
file_state = nil
-
branch_map = nil
-
io.each_line() do |line|
-
if state != "revision" && /^#{ENDLOG}/ =~ line
-
commit_log = String.new
-
revision = nil
-
state = "entry_start"
-
end
-
if state == "entry_start"
-
branch_map = Hash.new
-
if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project_locale)}(.+),v$/ =~ line
-
entry_path = normalize_cvs_path($1)
-
entry_name = normalize_path(File.basename($1))
-
logger.debug("Path #{entry_path} <=> Name #{entry_name}")
-
elsif /^head: (.+)$/ =~ line
-
entry_headRev = $1 #unless entry.nil?
-
elsif /^symbolic names:/ =~ line
-
state = "symbolic" #unless entry.nil?
-
elsif /^#{STARTLOG}/ =~ line
-
commit_log = String.new
-
state = "revision"
-
end
-
next
-
elsif state == "symbolic"
-
if /^(.*):\s(.*)/ =~ (line.strip)
-
branch_map[$1] = $2
-
else
-
state = "tags"
-
next
-
end
-
elsif state == "tags"
-
if /^#{STARTLOG}/ =~ line
-
commit_log = ""
-
state = "revision"
-
elsif /^#{ENDLOG}/ =~ line
-
state = "head"
-
end
-
next
-
elsif state == "revision"
-
if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
-
if revision
-
revHelper = CvsRevisionHelper.new(revision)
-
revBranch = "HEAD"
-
branch_map.each() do |branch_name, branch_point|
-
if revHelper.is_in_branch_with_symbol(branch_point)
-
revBranch = branch_name
-
end
-
end
-
logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
-
yield Revision.new({
-
:time => date,
-
:author => author,
-
:message => commit_log.chomp,
-
:paths => [{
-
:revision => revision.dup,
-
:branch => revBranch.dup,
-
:path => scm_iconv('UTF-8', @path_encoding, entry_path),
-
:name => scm_iconv('UTF-8', @path_encoding, entry_name),
-
:kind => 'file',
-
:action => file_state
-
}]
-
})
-
end
-
commit_log = String.new
-
revision = nil
-
if /^#{ENDLOG}/ =~ line
-
state = "entry_start"
-
end
-
next
-
end
-
-
if /^branches: (.+)$/ =~ line
-
# TODO: version.branch = $1
-
elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
-
revision = $1
-
elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
-
date = Time.parse($1)
-
line_utf8 = scm_iconv('UTF-8', options[:log_encoding], line)
-
author_utf8 = /author: ([^;]+)/.match(line_utf8)[1]
-
author = scm_iconv(options[:log_encoding], 'UTF-8', author_utf8)
-
file_state = /state: ([^;]+)/.match(line)[1]
-
# TODO:
-
# linechanges only available in CVS....
-
# maybe a feature our SVN implementation.
-
# I'm sure, they are useful for stats or something else
-
# linechanges =/lines: \+(\d+) -(\d+)/.match(line)
-
# unless linechanges.nil?
-
# version.line_plus = linechanges[1]
-
# version.line_minus = linechanges[2]
-
# else
-
# version.line_plus = 0
-
# version.line_minus = 0
-
# end
-
else
-
commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
-
end
-
end
-
end
-
end
-
rescue ScmCommandAborted
-
Revisions.new
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
logger.debug "<cvs> diff path:'#{path}'" +
-
",identifier_from #{identifier_from}, identifier_to #{identifier_to}"
-
cmd_args = %w|rdiff -u|
-
cmd_args << "-r#{identifier_to}"
-
cmd_args << "-r#{identifier_from}"
-
cmd_args << path_with_proj(path)
-
diff = []
-
scm_cmd(*cmd_args) do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
diff
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def cat(path, identifier=nil)
-
identifier = (identifier) ? identifier : "HEAD"
-
logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
-
cmd_args = %w|-q co|
-
cmd_args << "-D" << time_to_cvstime(identifier) if identifier
-
cmd_args << "-p" << path_with_proj(path)
-
cat = nil
-
scm_cmd(*cmd_args) do |io|
-
io.binmode
-
cat = io.read
-
end
-
cat
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def annotate(path, identifier=nil)
-
identifier = (identifier) ? identifier : "HEAD"
-
logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
-
cmd_args = %w|rannotate|
-
cmd_args << "-D" << time_to_cvstime(identifier) if identifier
-
cmd_args << path_with_proj(path)
-
blame = Annotate.new
-
scm_cmd(*cmd_args) do |io|
-
io.each_line do |line|
-
next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
-
blame.add_line(
-
$3.rstrip,
-
Revision.new(
-
:revision => $1,
-
:identifier => nil,
-
:author => $2.strip
-
))
-
end
-
end
-
blame
-
rescue ScmCommandAborted
-
Annotate.new
-
end
-
-
1
private
-
-
# Returns the root url without the connexion string
-
# :pserver:anonymous@foo.bar:/path => /path
-
# :ext:cvsservername:/path => /path
-
1
def root_url_path
-
root_url.to_s.gsub(/^:.+:\d*/, '')
-
end
-
-
# convert a date/time into the CVS-format
-
1
def time_to_cvstime(time)
-
return nil if time.nil?
-
time = Time.now if time == 'HEAD'
-
-
unless time.kind_of? Time
-
time = Time.parse(time)
-
end
-
return time_to_cvstime_rlog(time)
-
end
-
-
1
def time_to_cvstime_rlog(time)
-
return nil if time.nil?
-
t1 = time.clone.localtime
-
return t1.strftime("%Y-%m-%d %H:%M:%S")
-
end
-
-
1
def normalize_cvs_path(path)
-
normalize_path(path.gsub(/Attic\//,''))
-
end
-
-
1
def normalize_path(path)
-
path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
-
end
-
-
1
def path_with_proj(path)
-
"#{url}#{with_leading_slash(path)}"
-
end
-
1
private :path_with_proj
-
-
1
class Revision < Redmine::Scm::Adapters::Revision
-
# Returns the readable identifier
-
1
def format_identifier
-
revision.to_s
-
end
-
end
-
-
1
def scm_cmd(*args, &block)
-
full_args = ['-d', root_url]
-
full_args += args
-
full_args_locale = []
-
full_args.map do |e|
-
full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
-
end
-
ret = shellout(
-
self.class.sq_bin + ' ' + full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
-
&block
-
)
-
if $? && $?.exitstatus != 0
-
raise ScmCommandAborted, "cvs exited with non-zero status: #{$?.exitstatus}"
-
end
-
ret
-
end
-
1
private :scm_cmd
-
end
-
-
1
class CvsRevisionHelper
-
1
attr_accessor :complete_rev, :revision, :base, :branchid
-
-
1
def initialize(complete_rev)
-
@complete_rev = complete_rev
-
parseRevision()
-
end
-
-
1
def branchPoint
-
return @base
-
end
-
-
1
def branchVersion
-
if isBranchRevision
-
return @base+"."+@branchid
-
end
-
return @base
-
end
-
-
1
def isBranchRevision
-
!@branchid.nil?
-
end
-
-
1
def prevRev
-
unless @revision == 0
-
return buildRevision( @revision - 1 )
-
end
-
return buildRevision( @revision )
-
end
-
-
1
def is_in_branch_with_symbol(branch_symbol)
-
bpieces = branch_symbol.split(".")
-
branch_start = "#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
-
return ( branchVersion == branch_start )
-
end
-
-
1
private
-
1
def buildRevision(rev)
-
if rev == 0
-
if @branchid.nil?
-
@base + ".0"
-
else
-
@base
-
end
-
elsif @branchid.nil?
-
@base + "." + rev.to_s
-
else
-
@base + "." + @branchid + "." + rev.to_s
-
end
-
end
-
-
# Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
-
1
def parseRevision()
-
pieces = @complete_rev.split(".")
-
@revision = pieces.last.to_i
-
baseSize = 1
-
baseSize += (pieces.size / 2)
-
@base = pieces[0..-baseSize].join(".")
-
if baseSize > 2
-
@branchid = pieces[-2]
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
1
require 'rexml/document'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class DarcsAdapter < AbstractAdapter
-
# Darcs executable name
-
1
DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
-
-
1
class << self
-
1
def client_command
-
@@bin ||= DARCS_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (darcs_binary_version || [])
-
end
-
-
1
def client_available
-
!client_version.empty?
-
end
-
-
1
def darcs_binary_version
-
darcsversion = darcs_binary_version_from_command_line.dup
-
if darcsversion.respond_to?(:force_encoding)
-
darcsversion.force_encoding('ASCII-8BIT')
-
end
-
if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def darcs_binary_version_from_command_line
-
shellout("#{sq_bin} --version") { |io| io.read }.to_s
-
end
-
end
-
-
1
def initialize(url, root_url=nil, login=nil, password=nil,
-
path_encoding=nil)
-
@url = url
-
@root_url = url
-
end
-
-
1
def supports_cat?
-
# cat supported in darcs 2.0.0 and higher
-
self.class.client_version_above?([2, 0, 0])
-
end
-
-
# Get info about the darcs repository
-
1
def info
-
rev = revisions(nil,nil,nil,{:limit => 1})
-
rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
-
end
-
-
# Returns an Entries collection
-
# or nil if the given path doesn't exist in the repository
-
1
def entries(path=nil, identifier=nil, options={})
-
path_prefix = (path.blank? ? '' : "#{path}/")
-
if path.blank?
-
path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
-
end
-
entries = Entries.new
-
cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --xml-output"
-
cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
-
cmd << " #{shell_quote path}"
-
shellout(cmd) do |io|
-
begin
-
doc = REXML::Document.new(io)
-
if doc.root.name == 'directory'
-
doc.elements.each('directory/*') do |element|
-
next unless ['file', 'directory'].include? element.name
-
entries << entry_from_xml(element, path_prefix)
-
end
-
elsif doc.root.name == 'file'
-
entries << entry_from_xml(doc.root, path_prefix)
-
end
-
rescue
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
entries.compact!
-
entries.sort_by_name
-
end
-
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
path = '.' if path.blank?
-
revisions = Revisions.new
-
cmd = "#{self.class.sq_bin} changes --repodir #{shell_quote @url} --xml-output"
-
cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
-
cmd << " --last #{options[:limit].to_i}" if options[:limit]
-
shellout(cmd) do |io|
-
begin
-
doc = REXML::Document.new(io)
-
doc.elements.each("changelog/patch") do |patch|
-
message = patch.elements['name'].text
-
message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
-
revisions << Revision.new({:identifier => nil,
-
:author => patch.attributes['author'],
-
:scmid => patch.attributes['hash'],
-
:time => Time.parse(patch.attributes['local_date']),
-
:message => message,
-
:paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
-
})
-
end
-
rescue
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
revisions
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
path = '*' if path.blank?
-
cmd = "#{self.class.sq_bin} diff --repodir #{shell_quote @url}"
-
if identifier_to.nil?
-
cmd << " --match #{shell_quote("hash #{identifier_from}")}"
-
else
-
cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
-
cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
-
end
-
cmd << " -u #{shell_quote path}"
-
diff = []
-
shellout(cmd) do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
diff
-
end
-
-
1
def cat(path, identifier=nil)
-
cmd = "#{self.class.sq_bin} show content --repodir #{shell_quote @url}"
-
cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
-
cmd << " #{shell_quote path}"
-
cat = nil
-
shellout(cmd) do |io|
-
io.binmode
-
cat = io.read
-
end
-
return nil if $? && $?.exitstatus != 0
-
cat
-
end
-
-
1
private
-
-
# Returns an Entry from the given XML element
-
# or nil if the entry was deleted
-
1
def entry_from_xml(element, path_prefix)
-
modified_element = element.elements['modified']
-
if modified_element.elements['modified_how'].text.match(/removed/)
-
return nil
-
end
-
-
Entry.new({:name => element.attributes['name'],
-
:path => path_prefix + element.attributes['name'],
-
:kind => element.name == 'file' ? 'file' : 'dir',
-
:size => nil,
-
:lastrev => Revision.new({
-
:identifier => nil,
-
:scmid => modified_element.elements['patch'].attributes['hash']
-
})
-
})
-
end
-
-
1
def get_paths_for_patch(hash)
-
paths = get_paths_for_patch_raw(hash)
-
if self.class.client_version_above?([2, 4])
-
orig_paths = paths
-
paths = []
-
add_paths = []
-
add_paths_name = []
-
mod_paths = []
-
other_paths = []
-
orig_paths.each do |path|
-
if path[:action] == 'A'
-
add_paths << path
-
add_paths_name << path[:path]
-
elsif path[:action] == 'M'
-
mod_paths << path
-
else
-
other_paths << path
-
end
-
end
-
add_paths_name.each do |add_path|
-
mod_paths.delete_if { |m| m[:path] == add_path }
-
end
-
paths.concat add_paths
-
paths.concat mod_paths
-
paths.concat other_paths
-
end
-
paths
-
end
-
-
# Retrieve changed paths for a single patch
-
1
def get_paths_for_patch_raw(hash)
-
cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --summary --xml-output"
-
cmd << " --match #{shell_quote("hash #{hash}")} "
-
paths = []
-
shellout(cmd) do |io|
-
begin
-
# Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
-
# A root element is added so that REXML doesn't raise an error
-
doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
-
doc.elements.each('fake_root/summary/*') do |modif|
-
paths << {:action => modif.name[0,1].upcase,
-
:path => "/" + modif.text.chomp.gsub(/^\s*/, '')
-
}
-
end
-
rescue
-
end
-
end
-
paths
-
rescue CommandFailed
-
paths
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# FileSystem adapter
-
# File written by Paul Rivier, at Demotera.
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
1
require 'find'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class FilesystemAdapter < AbstractAdapter
-
-
1
class << self
-
1
def client_available
-
true
-
end
-
end
-
-
1
def initialize(url, root_url=nil, login=nil, password=nil,
-
path_encoding=nil)
-
@url = with_trailling_slash(url)
-
@path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
-
end
-
-
1
def path_encoding
-
@path_encoding
-
end
-
-
1
def format_path_ends(path, leading=true, trailling=true)
-
path = leading ? with_leading_slash(path) :
-
without_leading_slash(path)
-
trailling ? with_trailling_slash(path) :
-
without_trailling_slash(path)
-
end
-
-
1
def info
-
info = Info.new({:root_url => target(),
-
:lastrev => nil
-
})
-
info
-
rescue CommandFailed
-
return nil
-
end
-
-
1
def entries(path="", identifier=nil, options={})
-
entries = Entries.new
-
trgt_utf8 = target(path)
-
trgt = scm_iconv(@path_encoding, 'UTF-8', trgt_utf8)
-
Dir.new(trgt).each do |e1|
-
e_utf8 = scm_iconv('UTF-8', @path_encoding, e1)
-
next if e_utf8.blank?
-
relative_path_utf8 = format_path_ends(
-
(format_path_ends(path,false,true) + e_utf8),false,false)
-
t1_utf8 = target(relative_path_utf8)
-
t1 = scm_iconv(@path_encoding, 'UTF-8', t1_utf8)
-
relative_path = scm_iconv(@path_encoding, 'UTF-8', relative_path_utf8)
-
e1 = scm_iconv(@path_encoding, 'UTF-8', e_utf8)
-
if File.exist?(t1) and # paranoid test
-
%w{file directory}.include?(File.ftype(t1)) and # avoid special types
-
not File.basename(e1).match(/^\.+$/) # avoid . and ..
-
p1 = File.readable?(t1) ? relative_path : ""
-
utf_8_path = scm_iconv('UTF-8', @path_encoding, p1)
-
entries <<
-
Entry.new({ :name => scm_iconv('UTF-8', @path_encoding, File.basename(e1)),
-
# below : list unreadable files, but dont link them.
-
:path => utf_8_path,
-
:kind => (File.directory?(t1) ? 'dir' : 'file'),
-
:size => (File.directory?(t1) ? nil : [File.size(t1)].pack('l').unpack('L').first),
-
:lastrev =>
-
Revision.new({:time => (File.mtime(t1)) })
-
})
-
end
-
end
-
entries.sort_by_name
-
rescue => err
-
logger.error "scm: filesystem: error: #{err.message}"
-
raise CommandFailed.new(err.message)
-
end
-
-
1
def cat(path, identifier=nil)
-
p = scm_iconv(@path_encoding, 'UTF-8', target(path))
-
File.new(p, "rb").read
-
rescue => err
-
logger.error "scm: filesystem: error: #{err.message}"
-
raise CommandFailed.new(err.message)
-
end
-
-
1
private
-
-
# AbstractAdapter::target is implicitly made to quote paths.
-
# Here we do not shell-out, so we do not want quotes.
-
1
def target(path=nil)
-
# Prevent the use of ..
-
if path and !path.match(/(^|\/)\.\.(\/|$)/)
-
return "#{self.url}#{without_leading_slash(path)}"
-
end
-
return self.url
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class GitAdapter < AbstractAdapter
-
-
# Git executable name
-
1
GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
-
-
1
class GitBranch < Branch
-
1
attr_accessor :is_default
-
end
-
-
1
class << self
-
1
def client_command
-
@@bin ||= GIT_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (scm_command_version || [])
-
end
-
-
1
def client_available
-
!client_version.empty?
-
end
-
-
1
def scm_command_version
-
scm_version = scm_version_from_command_line.dup
-
if scm_version.respond_to?(:force_encoding)
-
scm_version.force_encoding('ASCII-8BIT')
-
end
-
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def scm_version_from_command_line
-
shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
-
end
-
end
-
-
1
def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
-
super
-
@path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
-
end
-
-
1
def path_encoding
-
@path_encoding
-
end
-
-
1
def info
-
begin
-
Info.new(:root_url => url, :lastrev => lastrev('',nil))
-
rescue
-
nil
-
end
-
end
-
-
1
def branches
-
return @branches if @branches
-
@branches = []
-
cmd_args = %w|branch --no-color --verbose --no-abbrev|
-
git_cmd(cmd_args) do |io|
-
io.each_line do |line|
-
branch_rev = line.match('\s*(\*?)\s*(.*?)\s*([0-9a-f]{40}).*$')
-
bran = GitBranch.new(branch_rev[2])
-
bran.revision = branch_rev[3]
-
bran.scmid = branch_rev[3]
-
bran.is_default = ( branch_rev[1] == '*' )
-
@branches << bran
-
end
-
end
-
@branches.sort!
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def tags
-
return @tags if @tags
-
cmd_args = %w|tag|
-
git_cmd(cmd_args) do |io|
-
@tags = io.readlines.sort!.map{|t| t.strip}
-
end
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def default_branch
-
bras = self.branches
-
return nil if bras.nil?
-
default_bras = bras.select{|x| x.is_default == true}
-
return default_bras.first.to_s if ! default_bras.empty?
-
master_bras = bras.select{|x| x.to_s == 'master'}
-
master_bras.empty? ? bras.first.to_s : 'master'
-
end
-
-
1
def entry(path=nil, identifier=nil)
-
parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
-
search_path = parts[0..-2].join('/')
-
search_name = parts[-1]
-
if search_path.blank? && search_name.blank?
-
# Root entry
-
Entry.new(:path => '', :kind => 'dir')
-
else
-
# Search for the entry in the parent directory
-
es = entries(search_path, identifier,
-
options = {:report_last_commit => false})
-
es ? es.detect {|e| e.name == search_name} : nil
-
end
-
end
-
-
1
def entries(path=nil, identifier=nil, options={})
-
path ||= ''
-
p = scm_iconv(@path_encoding, 'UTF-8', path)
-
entries = Entries.new
-
cmd_args = %w|ls-tree -l|
-
cmd_args << "HEAD:#{p}" if identifier.nil?
-
cmd_args << "#{identifier}:#{p}" if identifier
-
git_cmd(cmd_args) do |io|
-
io.each_line do |line|
-
e = line.chomp.to_s
-
if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
-
type = $1
-
sha = $2
-
size = $3
-
name = $4
-
if name.respond_to?(:force_encoding)
-
name.force_encoding(@path_encoding)
-
end
-
full_path = p.empty? ? name : "#{p}/#{name}"
-
n = scm_iconv('UTF-8', @path_encoding, name)
-
full_p = scm_iconv('UTF-8', @path_encoding, full_path)
-
entries << Entry.new({:name => n,
-
:path => full_p,
-
:kind => (type == "tree") ? 'dir' : 'file',
-
:size => (type == "tree") ? nil : size,
-
:lastrev => options[:report_last_commit] ?
-
lastrev(full_path, identifier) : Revision.new
-
}) unless entries.detect{|entry| entry.name == name}
-
end
-
end
-
end
-
entries.sort_by_name
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def lastrev(path, rev)
-
return nil if path.nil?
-
cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
-
cmd_args << rev if rev
-
cmd_args << "--" << path unless path.empty?
-
lines = []
-
git_cmd(cmd_args) { |io| lines = io.readlines }
-
begin
-
id = lines[0].split[1]
-
author = lines[1].match('Author:\s+(.*)$')[1]
-
time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
-
-
Revision.new({
-
:identifier => id,
-
:scmid => id,
-
:author => author,
-
:time => time,
-
:message => nil,
-
:paths => nil
-
})
-
rescue NoMethodError => e
-
logger.error("The revision '#{path}' has a wrong format")
-
return nil
-
end
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def revisions(path, identifier_from, identifier_to, options={})
-
revs = Revisions.new
-
cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin|
-
cmd_args << "--reverse" if options[:reverse]
-
cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
-
cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
-
revisions = []
-
if identifier_from || identifier_to
-
revisions << ""
-
revisions[0] << "#{identifier_from}.." if identifier_from
-
revisions[0] << "#{identifier_to}" if identifier_to
-
else
-
unless options[:includes].blank?
-
revisions += options[:includes]
-
end
-
unless options[:excludes].blank?
-
revisions += options[:excludes].map{|r| "^#{r}"}
-
end
-
end
-
-
git_cmd(cmd_args, {:write_stdin => true}) do |io|
-
io.binmode
-
io.puts(revisions.join("\n"))
-
io.close_write
-
files=[]
-
changeset = {}
-
parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
-
-
io.each_line do |line|
-
if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/
-
key = "commit"
-
value = $1
-
parents_str = $2
-
if (parsing_descr == 1 || parsing_descr == 2)
-
parsing_descr = 0
-
revision = Revision.new({
-
:identifier => changeset[:commit],
-
:scmid => changeset[:commit],
-
:author => changeset[:author],
-
:time => Time.parse(changeset[:date]),
-
:message => changeset[:description],
-
:paths => files,
-
:parents => changeset[:parents]
-
})
-
if block_given?
-
yield revision
-
else
-
revs << revision
-
end
-
changeset = {}
-
files = []
-
end
-
changeset[:commit] = $1
-
unless parents_str.nil? or parents_str == ""
-
changeset[:parents] = parents_str.strip.split(' ')
-
end
-
elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
-
key = $1
-
value = $2
-
if key == "Author"
-
changeset[:author] = value
-
elsif key == "CommitDate"
-
changeset[:date] = value
-
end
-
elsif (parsing_descr == 0) && line.chomp.to_s == ""
-
parsing_descr = 1
-
changeset[:description] = ""
-
elsif (parsing_descr == 1 || parsing_descr == 2) \
-
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
-
parsing_descr = 2
-
fileaction = $1
-
filepath = $2
-
p = scm_iconv('UTF-8', @path_encoding, filepath)
-
files << {:action => fileaction, :path => p}
-
elsif (parsing_descr == 1 || parsing_descr == 2) \
-
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
-
parsing_descr = 2
-
fileaction = $1
-
filepath = $3
-
p = scm_iconv('UTF-8', @path_encoding, filepath)
-
files << {:action => fileaction, :path => p}
-
elsif (parsing_descr == 1) && line.chomp.to_s == ""
-
parsing_descr = 2
-
elsif (parsing_descr == 1)
-
changeset[:description] << line[4..-1]
-
end
-
end
-
-
if changeset[:commit]
-
revision = Revision.new({
-
:identifier => changeset[:commit],
-
:scmid => changeset[:commit],
-
:author => changeset[:author],
-
:time => Time.parse(changeset[:date]),
-
:message => changeset[:description],
-
:paths => files,
-
:parents => changeset[:parents]
-
})
-
if block_given?
-
yield revision
-
else
-
revs << revision
-
end
-
end
-
end
-
revs
-
rescue ScmCommandAborted => e
-
err_msg = "git log error: #{e.message}"
-
logger.error(err_msg)
-
if block_given?
-
raise CommandFailed, err_msg
-
else
-
revs
-
end
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
path ||= ''
-
cmd_args = []
-
if identifier_to
-
cmd_args << "diff" << "--no-color" << identifier_to << identifier_from
-
else
-
cmd_args << "show" << "--no-color" << identifier_from
-
end
-
cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
-
diff = []
-
git_cmd(cmd_args) do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
diff
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def annotate(path, identifier=nil)
-
identifier = 'HEAD' if identifier.blank?
-
cmd_args = %w|blame|
-
cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
-
blame = Annotate.new
-
content = nil
-
git_cmd(cmd_args) { |io| io.binmode; content = io.read }
-
# git annotates binary files
-
return nil if content.is_binary_data?
-
identifier = ''
-
# git shows commit author on the first occurrence only
-
authors_by_commit = {}
-
content.split("\n").each do |line|
-
if line =~ /^([0-9a-f]{39,40})\s.*/
-
identifier = $1
-
elsif line =~ /^author (.+)/
-
authors_by_commit[identifier] = $1.strip
-
elsif line =~ /^\t(.*)/
-
blame.add_line($1, Revision.new(
-
:identifier => identifier,
-
:revision => identifier,
-
:scmid => identifier,
-
:author => authors_by_commit[identifier]
-
))
-
identifier = ''
-
author = ''
-
end
-
end
-
blame
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def cat(path, identifier=nil)
-
if identifier.nil?
-
identifier = 'HEAD'
-
end
-
cmd_args = %w|show --no-color|
-
cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
-
cat = nil
-
git_cmd(cmd_args) do |io|
-
io.binmode
-
cat = io.read
-
end
-
cat
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
class Revision < Redmine::Scm::Adapters::Revision
-
# Returns the readable identifier
-
1
def format_identifier
-
identifier[0,8]
-
end
-
end
-
-
1
def git_cmd(args, options = {}, &block)
-
repo_path = root_url || url
-
full_args = ['--git-dir', repo_path]
-
if self.class.client_version_above?([1, 7, 2])
-
full_args << '-c' << 'core.quotepath=false'
-
full_args << '-c' << 'log.decorate=no'
-
end
-
full_args += args
-
ret = shellout(
-
self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
-
options,
-
&block
-
)
-
if $? && $?.exitstatus != 0
-
raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
-
end
-
ret
-
end
-
1
private :git_cmd
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
1
require 'cgi'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class MercurialAdapter < AbstractAdapter
-
-
# Mercurial executable name
-
1
HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
-
1
HELPERS_DIR = File.dirname(__FILE__) + "/mercurial"
-
1
HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py"
-
1
TEMPLATE_NAME = "hg-template"
-
1
TEMPLATE_EXTENSION = "tmpl"
-
-
# raised if hg command exited with error, e.g. unknown revision.
-
1
class HgCommandAborted < CommandFailed; end
-
-
1
class << self
-
1
def client_command
-
@@bin ||= HG_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (hgversion || [])
-
end
-
-
1
def client_available
-
client_version_above?([1, 2])
-
end
-
-
1
def hgversion
-
# The hg version is expressed either as a
-
# release number (eg 0.9.5 or 1.0) or as a revision
-
# id composed of 12 hexa characters.
-
theversion = hgversion_from_command_line.dup
-
if theversion.respond_to?(:force_encoding)
-
theversion.force_encoding('ASCII-8BIT')
-
end
-
if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def hgversion_from_command_line
-
shellout("#{sq_bin} --version") { |io| io.read }.to_s
-
end
-
-
1
def template_path
-
@@template_path ||= template_path_for(client_version)
-
end
-
-
1
def template_path_for(version)
-
"#{HELPERS_DIR}/#{TEMPLATE_NAME}-1.0.#{TEMPLATE_EXTENSION}"
-
end
-
end
-
-
1
def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
-
super
-
@path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
-
end
-
-
1
def path_encoding
-
@path_encoding
-
end
-
-
1
def info
-
tip = summary['repository']['tip']
-
Info.new(:root_url => CGI.unescape(summary['repository']['root']),
-
:lastrev => Revision.new(:revision => tip['revision'],
-
:scmid => tip['node']))
-
# rescue HgCommandAborted
-
rescue Exception => e
-
logger.error "hg: error during getting info: #{e.message}"
-
nil
-
end
-
-
1
def tags
-
as_ary(summary['repository']['tag']).map { |e| e['name'] }
-
end
-
-
# Returns map of {'tag' => 'nodeid', ...}
-
1
def tagmap
-
alist = as_ary(summary['repository']['tag']).map do |e|
-
e.values_at('name', 'node')
-
end
-
Hash[*alist.flatten]
-
end
-
-
1
def branches
-
brs = []
-
as_ary(summary['repository']['branch']).each do |e|
-
br = Branch.new(e['name'])
-
br.revision = e['revision']
-
br.scmid = e['node']
-
brs << br
-
end
-
brs
-
end
-
-
# Returns map of {'branch' => 'nodeid', ...}
-
1
def branchmap
-
alist = as_ary(summary['repository']['branch']).map do |e|
-
e.values_at('name', 'node')
-
end
-
Hash[*alist.flatten]
-
end
-
-
1
def summary
-
return @summary if @summary
-
hg 'rhsummary' do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
@summary = parse_xml(output)['rhsummary']
-
rescue
-
end
-
end
-
end
-
1
private :summary
-
-
1
def entries(path=nil, identifier=nil, options={})
-
p1 = scm_iconv(@path_encoding, 'UTF-8', path)
-
manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)),
-
CGI.escape(without_leading_slash(p1.to_s))) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
parse_xml(output)['rhmanifest']['repository']['manifest']
-
rescue
-
end
-
end
-
path_prefix = path.blank? ? '' : with_trailling_slash(path)
-
-
entries = Entries.new
-
as_ary(manifest['dir']).each do |e|
-
n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
-
p = "#{path_prefix}#{n}"
-
entries << Entry.new(:name => n, :path => p, :kind => 'dir')
-
end
-
-
as_ary(manifest['file']).each do |e|
-
n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
-
p = "#{path_prefix}#{n}"
-
lr = Revision.new(:revision => e['revision'], :scmid => e['node'],
-
:identifier => e['node'],
-
:time => Time.at(e['time'].to_i))
-
entries << Entry.new(:name => n, :path => p, :kind => 'file',
-
:size => e['size'].to_i, :lastrev => lr)
-
end
-
-
entries
-
rescue HgCommandAborted
-
nil # means not found
-
end
-
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
revs = Revisions.new
-
each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
-
revs
-
end
-
-
# Iterates the revisions by using a template file that
-
# makes Mercurial produce a xml output.
-
1
def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
-
hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
-
hg_args << '--limit' << options[:limit] if options[:limit]
-
hg_args << hgtarget(path) unless path.blank?
-
log = hg(*hg_args) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
# Mercurial < 1.5 does not support footer template for '</log>'
-
parse_xml("#{output}</log>")['log']
-
rescue
-
end
-
end
-
as_ary(log['logentry']).each do |le|
-
cpalist = as_ary(le['paths']['path-copied']).map do |e|
-
[e['__content__'], e['copyfrom-path']].map do |s|
-
scm_iconv('UTF-8', @path_encoding, CGI.unescape(s))
-
end
-
end
-
cpmap = Hash[*cpalist.flatten]
-
paths = as_ary(le['paths']['path']).map do |e|
-
p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
-
{:action => e['action'],
-
:path => with_leading_slash(p),
-
:from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
-
:from_revision => (cpmap.member?(p) ? le['node'] : nil)}
-
end.sort { |a, b| a[:path] <=> b[:path] }
-
parents_ary = []
-
as_ary(le['parents']['parent']).map do |par|
-
parents_ary << par['__content__'] if par['__content__'] != "000000000000"
-
end
-
yield Revision.new(:revision => le['revision'],
-
:scmid => le['node'],
-
:author => (le['author']['__content__'] rescue ''),
-
:time => Time.parse(le['date']['__content__']),
-
:message => le['msg']['__content__'],
-
:paths => paths,
-
:parents => parents_ary)
-
end
-
self
-
end
-
-
# Returns list of nodes in the specified branch
-
1
def nodes_in_branch(branch, options={})
-
hg_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)]
-
hg_args << '--from' << CGI.escape(branch)
-
hg_args << '--to' << '0'
-
hg_args << '--limit' << options[:limit] if options[:limit]
-
hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
hg_args = %w|rhdiff|
-
if identifier_to
-
hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
-
else
-
hg_args << '-c' << hgrev(identifier_from)
-
end
-
unless path.blank?
-
p = scm_iconv(@path_encoding, 'UTF-8', path)
-
hg_args << CGI.escape(hgtarget(p))
-
end
-
diff = []
-
hg *hg_args do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
diff
-
rescue HgCommandAborted
-
nil # means not found
-
end
-
-
1
def cat(path, identifier=nil)
-
p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
-
hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
-
io.binmode
-
io.read
-
end
-
rescue HgCommandAborted
-
nil # means not found
-
end
-
-
1
def annotate(path, identifier=nil)
-
p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
-
blame = Annotate.new
-
hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
-
io.each_line do |line|
-
line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding)
-
next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
-
r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
-
:identifier => $3)
-
blame.add_line($4.rstrip, r)
-
end
-
end
-
blame
-
rescue HgCommandAborted
-
# means not found or cannot be annotated
-
Annotate.new
-
end
-
-
1
class Revision < Redmine::Scm::Adapters::Revision
-
# Returns the readable identifier
-
1
def format_identifier
-
"#{revision}:#{scmid}"
-
end
-
end
-
-
# Runs 'hg' command with the given args
-
1
def hg(*args, &block)
-
repo_path = root_url || url
-
full_args = ['-R', repo_path, '--encoding', 'utf-8']
-
full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
-
full_args << '--config' << 'diff.git=false'
-
full_args += args
-
ret = shellout(
-
self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
-
&block
-
)
-
if $? && $?.exitstatus != 0
-
raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
-
end
-
ret
-
end
-
1
private :hg
-
-
# Returns correct revision identifier
-
1
def hgrev(identifier, sq=false)
-
rev = identifier.blank? ? 'tip' : identifier.to_s
-
rev = shell_quote(rev) if sq
-
rev
-
end
-
1
private :hgrev
-
-
1
def hgtarget(path)
-
path ||= ''
-
root_url + '/' + without_leading_slash(path)
-
end
-
1
private :hgtarget
-
-
1
def as_ary(o)
-
return [] unless o
-
o.is_a?(Array) ? o : Array[o]
-
end
-
1
private :as_ary
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
1
require 'uri'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class SubversionAdapter < AbstractAdapter
-
-
# SVN executable name
-
1
SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn"
-
-
1
class << self
-
1
def client_command
-
@@bin ||= SVN_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (svn_binary_version || [])
-
end
-
-
1
def client_available
-
# --xml options are introduced in 1.3.
-
# http://subversion.apache.org/docs/release-notes/1.3.html
-
client_version_above?([1, 3])
-
end
-
-
1
def svn_binary_version
-
scm_version = scm_version_from_command_line.dup
-
if scm_version.respond_to?(:force_encoding)
-
scm_version.force_encoding('ASCII-8BIT')
-
end
-
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def scm_version_from_command_line
-
shellout("#{sq_bin} --version") { |io| io.read }.to_s
-
end
-
end
-
-
# Get info about the svn repository
-
1
def info
-
cmd = "#{self.class.sq_bin} info --xml #{target}"
-
cmd << credentials_string
-
info = nil
-
shellout(cmd) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
doc = parse_xml(output)
-
# root_url = doc.elements["info/entry/repository/root"].text
-
info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
-
:lastrev => Revision.new({
-
:identifier => doc['info']['entry']['commit']['revision'],
-
:time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
-
:author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
-
})
-
})
-
rescue
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
info
-
rescue CommandFailed
-
return nil
-
end
-
-
# Returns an Entries collection
-
# or nil if the given path doesn't exist in the repository
-
1
def entries(path=nil, identifier=nil, options={})
-
path ||= ''
-
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
-
entries = Entries.new
-
cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
-
cmd << credentials_string
-
shellout(cmd) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
doc = parse_xml(output)
-
each_xml_element(doc['lists']['list'], 'entry') do |entry|
-
commit = entry['commit']
-
commit_date = commit['date']
-
# Skip directory if there is no commit date (usually that
-
# means that we don't have read access to it)
-
next if entry['kind'] == 'dir' && commit_date.nil?
-
name = entry['name']['__content__']
-
entries << Entry.new({:name => URI.unescape(name),
-
:path => ((path.empty? ? "" : "#{path}/") + name),
-
:kind => entry['kind'],
-
:size => ((s = entry['size']) ? s['__content__'].to_i : nil),
-
:lastrev => Revision.new({
-
:identifier => commit['revision'],
-
:time => Time.parse(commit_date['__content__'].to_s).localtime,
-
:author => ((a = commit['author']) ? a['__content__'] : nil)
-
})
-
})
-
end
-
rescue Exception => e
-
logger.error("Error parsing svn output: #{e.message}")
-
logger.error("Output was:\n #{output}")
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
-
entries.sort_by_name
-
end
-
-
1
def properties(path, identifier=nil)
-
# proplist xml output supported in svn 1.5.0 and higher
-
return nil unless self.class.client_version_above?([1, 5, 0])
-
-
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
-
cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
-
cmd << credentials_string
-
properties = {}
-
shellout(cmd) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
doc = parse_xml(output)
-
each_xml_element(doc['properties']['target'], 'property') do |property|
-
properties[ property['name'] ] = property['__content__'].to_s
-
end
-
rescue
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
properties
-
end
-
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
path ||= ''
-
identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
-
identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
-
revisions = Revisions.new
-
cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
-
cmd << credentials_string
-
cmd << " --verbose " if options[:with_paths]
-
cmd << " --limit #{options[:limit].to_i}" if options[:limit]
-
cmd << ' ' + target(path)
-
shellout(cmd) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
doc = parse_xml(output)
-
each_xml_element(doc['log'], 'logentry') do |logentry|
-
paths = []
-
each_xml_element(logentry['paths'], 'path') do |path|
-
paths << {:action => path['action'],
-
:path => path['__content__'],
-
:from_path => path['copyfrom-path'],
-
:from_revision => path['copyfrom-rev']
-
}
-
end if logentry['paths'] && logentry['paths']['path']
-
paths.sort! { |x,y| x[:path] <=> y[:path] }
-
-
revisions << Revision.new({:identifier => logentry['revision'],
-
:author => (logentry['author'] ? logentry['author']['__content__'] : ""),
-
:time => Time.parse(logentry['date']['__content__'].to_s).localtime,
-
:message => logentry['msg']['__content__'],
-
:paths => paths
-
})
-
end
-
rescue
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
revisions
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
path ||= ''
-
identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
-
-
identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
-
-
cmd = "#{self.class.sq_bin} diff -r "
-
cmd << "#{identifier_to}:"
-
cmd << "#{identifier_from}"
-
cmd << " #{target(path)}@#{identifier_from}"
-
cmd << credentials_string
-
diff = []
-
shellout(cmd) do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
diff
-
end
-
-
1
def cat(path, identifier=nil)
-
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
-
cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
-
cmd << credentials_string
-
cat = nil
-
shellout(cmd) do |io|
-
io.binmode
-
cat = io.read
-
end
-
return nil if $? && $?.exitstatus != 0
-
cat
-
end
-
-
1
def annotate(path, identifier=nil)
-
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
-
cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
-
cmd << credentials_string
-
blame = Annotate.new
-
shellout(cmd) do |io|
-
io.each_line do |line|
-
next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
-
rev = $1
-
blame.add_line($3.rstrip,
-
Revision.new(
-
:identifier => rev,
-
:revision => rev,
-
:author => $2.strip
-
))
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
blame
-
end
-
-
1
private
-
-
1
def credentials_string
-
str = ''
-
str << " --username #{shell_quote(@login)}" unless @login.blank?
-
str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
-
str << " --no-auth-cache --non-interactive"
-
str
-
end
-
-
# Helper that iterates over the child elements of a xml node
-
# MiniXml returns a hash when a single child is found
-
# or an array of hashes for multiple children
-
1
def each_xml_element(node, name)
-
if node && node[name]
-
if node[name].is_a?(Hash)
-
yield node[name]
-
else
-
node[name].each do |element|
-
yield element
-
end
-
end
-
end
-
end
-
-
1
def target(path = '')
-
base = path.match(/^\//) ? root_url : url
-
uri = "#{base}/#{path}"
-
uri = URI.escape(URI.escape(uri), '[]')
-
shell_quote(uri.gsub(/[?<>\*]/, ''))
-
end
-
end
-
end
-
end
-
end
-
1
module Redmine
-
1
module Scm
-
1
class Base
-
1
class << self
-
-
1
def all
-
@scms
-
end
-
-
# Add a new SCM adapter and repository
-
1
def add(scm_name)
-
7
@scms ||= []
-
7
@scms << scm_name
-
end
-
-
# Remove a SCM adapter from Redmine's list of supported scms
-
1
def delete(scm_name)
-
@scms.delete(scm_name)
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Search
-
-
1
mattr_accessor :available_search_types
-
-
1
@@available_search_types = []
-
-
1
class << self
-
1
def map(&block)
-
1
yield self
-
end
-
-
# Registers a search provider
-
1
def register(search_type, options={})
-
7
search_type = search_type.to_s
-
7
@@available_search_types << search_type unless @@available_search_types.include?(search_type)
-
end
-
end
-
-
1
module Controller
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
20
@@default_search_scopes = Hash.new {|hash, key| hash[key] = {:default => nil, :actions => {}}}
-
1
mattr_accessor :default_search_scopes
-
-
# Set the default search scope for a controller or specific actions
-
# Examples:
-
# * search_scope :issues # => sets the search scope to :issues for the whole controller
-
# * search_scope :issues, :only => :index
-
# * search_scope :issues, :only => [:index, :show]
-
1
def default_search_scope(id, options = {})
-
8
if actions = options[:only]
-
actions = [] << actions unless actions.is_a?(Array)
-
actions.each {|a| default_search_scopes[controller_name.to_sym][:actions][a.to_sym] = id.to_s}
-
else
-
8
default_search_scopes[controller_name.to_sym][:default] = id.to_s
-
end
-
end
-
end
-
-
1
def default_search_scopes
-
748
self.class.default_search_scopes
-
end
-
-
# Returns the default search scope according to the current action
-
1
def default_search_scope
-
@default_search_scope ||= default_search_scopes[controller_name.to_sym][:actions][action_name.to_sym] ||
-
399
default_search_scopes[controller_name.to_sym][:default]
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module SubclassFactory
-
1
def self.included(base)
-
3
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def get_subclass(class_name)
-
klass = nil
-
begin
-
klass = class_name.to_s.classify.constantize
-
rescue
-
# invalid class name
-
end
-
unless subclasses.include? klass
-
klass = nil
-
end
-
klass
-
end
-
-
# Returns an instance of the given subclass name
-
1
def new_subclass_instance(class_name, *args)
-
klass = get_subclass(class_name)
-
if klass
-
klass.new(*args)
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Themes
-
-
# Return an array of installed themes
-
1
def self.themes
-
@@installed_themes ||= scan_themes
-
end
-
-
# Rescan themes directory
-
1
def self.rescan
-
@@installed_themes = scan_themes
-
end
-
-
# Return theme for given id, or nil if it's not found
-
1
def self.theme(id, options={})
-
786
return nil if id.blank?
-
-
found = themes.find {|t| t.id == id}
-
if found.nil? && options[:rescan] != false
-
rescan
-
found = theme(id, :rescan => false)
-
end
-
found
-
end
-
-
# Class used to represent a theme
-
1
class Theme
-
1
attr_reader :path, :name, :dir
-
-
1
def initialize(path)
-
@path = path
-
@dir = File.basename(path)
-
@name = @dir.humanize
-
@stylesheets = nil
-
@javascripts = nil
-
end
-
-
# Directory name used as the theme id
-
1
def id; dir end
-
-
1
def ==(theme)
-
theme.is_a?(Theme) && theme.dir == dir
-
end
-
-
1
def <=>(theme)
-
name <=> theme.name
-
end
-
-
1
def stylesheets
-
@stylesheets ||= assets("stylesheets", "css")
-
end
-
-
1
def images
-
@images ||= assets("images")
-
end
-
-
1
def javascripts
-
@javascripts ||= assets("javascripts", "js")
-
end
-
-
1
def stylesheet_path(source)
-
"/themes/#{dir}/stylesheets/#{source}"
-
end
-
-
1
def image_path(source)
-
"/themes/#{dir}/images/#{source}"
-
end
-
-
1
def javascript_path(source)
-
"/themes/#{dir}/javascripts/#{source}"
-
end
-
-
1
private
-
-
1
def assets(dir, ext=nil)
-
if ext
-
Dir.glob("#{path}/#{dir}/*.#{ext}").collect {|f| File.basename(f).gsub(/\.#{ext}$/, '')}
-
else
-
Dir.glob("#{path}/#{dir}/*").collect {|f| File.basename(f)}
-
end
-
end
-
end
-
-
1
private
-
-
1
def self.scan_themes
-
dirs = Dir.glob("#{Rails.public_path}/themes/*").select do |f|
-
# A theme should at least override application.css
-
File.directory?(f) && File.exist?("#{f}/stylesheets/application.css")
-
end
-
dirs.collect {|dir| Theme.new(dir)}.sort
-
end
-
end
-
end
-
-
1
module ApplicationHelper
-
1
def current_theme
-
1498
unless instance_variable_defined?(:@current_theme)
-
412
@current_theme = Redmine::Themes.theme(Setting.ui_theme)
-
end
-
1498
@current_theme
-
end
-
-
# Returns the header tags for the current theme
-
1
def heads_for_theme
-
374
if current_theme && current_theme.javascripts.include?('theme')
-
javascript_include_tag current_theme.javascript_path('theme')
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Utils
-
1
class << self
-
# Returns the relative root url of the application
-
1
def relative_url_root
-
37
ActionController::Base.respond_to?('relative_url_root') ?
-
ActionController::Base.relative_url_root.to_s :
-
ActionController::Base.config.relative_url_root.to_s
-
end
-
-
# Sets the relative root url of the application
-
1
def relative_url_root=(arg)
-
if ActionController::Base.respond_to?('relative_url_root=')
-
ActionController::Base.relative_url_root=arg
-
else
-
ActionController::Base.config.relative_url_root = arg
-
end
-
end
-
-
# Generates a n bytes random hex string
-
# Example:
-
# random_hex(4) # => "89b8c729"
-
1
def random_hex(n)
-
130
SecureRandom.hex(n)
-
end
-
end
-
-
1
module Shell
-
1
def shell_quote(str)
-
if Redmine::Platform.mswin?
-
'"' + str.gsub(/"/, '\\"') + '"'
-
else
-
"'" + str.gsub(/'/, "'\"'\"'") + "'"
-
end
-
end
-
end
-
-
1
module DateCalculation
-
# Returns the number of working days between from and to
-
1
def working_days(from, to)
-
days = (to - from).to_i
-
if days > 0
-
weeks = days / 7
-
result = weeks * (7 - non_working_week_days.size)
-
days_left = days - weeks * 7
-
start_cwday = from.cwday
-
days_left.times do |i|
-
unless non_working_week_days.include?(((start_cwday + i - 1) % 7) + 1)
-
result += 1
-
end
-
end
-
result
-
else
-
0
-
end
-
end
-
-
# Adds working days to the given date
-
1
def add_working_days(date, working_days)
-
if working_days > 0
-
weeks = working_days / (7 - non_working_week_days.size)
-
result = weeks * 7
-
days_left = working_days - weeks * (7 - non_working_week_days.size)
-
cwday = date.cwday
-
while days_left > 0
-
cwday += 1
-
unless non_working_week_days.include?(((cwday - 1) % 7) + 1)
-
days_left -= 1
-
end
-
result += 1
-
end
-
next_working_date(date + result)
-
else
-
date
-
end
-
end
-
-
# Returns the date of the first day on or after the given date that is a working day
-
1
def next_working_date(date)
-
cwday = date.cwday
-
days = 0
-
while non_working_week_days.include?(((cwday + days - 1) % 7) + 1)
-
days += 1
-
end
-
date + days
-
end
-
-
# Returns the index of non working week days (1=monday, 7=sunday)
-
1
def non_working_week_days
-
@non_working_week_days ||= begin
-
days = Setting.non_working_week_days
-
if days.is_a?(Array) && days.size < 7
-
days.map(&:to_i)
-
else
-
[]
-
end
-
end
-
end
-
end
-
end
-
end
-
1
require 'rexml/document'
-
-
1
module Redmine
-
1
module VERSION #:nodoc:
-
1
MAJOR = 2
-
1
MINOR = 2
-
1
TINY = 3
-
-
# Branch values:
-
# * official release: nil
-
# * stable branch: stable
-
# * trunk: devel
-
1
BRANCH = 'stable'
-
-
# Retrieves the revision from the working copy
-
1
def self.revision
-
1
if File.directory?(File.join(Rails.root, '.svn'))
-
begin
-
path = Redmine::Scm::Adapters::AbstractAdapter.shell_quote(Rails.root.to_s)
-
if `svn info --xml #{path}` =~ /revision="(\d+)"/
-
return $1.to_i
-
end
-
rescue
-
# Could not find the current revision
-
end
-
end
-
nil
-
end
-
-
1
REVISION = self.revision
-
1
ARRAY = [MAJOR, MINOR, TINY, BRANCH, REVISION].compact
-
1
STRING = ARRAY.join('.')
-
-
1
def self.to_a; ARRAY end
-
4200
def self.to_s; STRING end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Views
-
1
class ApiTemplateHandler
-
1
def self.call(template)
-
"Redmine::Views::Builders.for(params[:format], request, response) do |api|; #{template.source}; self.output_buffer = api.output; end"
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'action_view/helpers/form_helper'
-
-
1
class Redmine::Views::LabelledFormBuilder < ActionView::Helpers::FormBuilder
-
1
include Redmine::I18n
-
-
(field_helpers.map(&:to_s) - %w(radio_button hidden_field fields_for) +
-
1
%w(date_select)).each do |selector|
-
14
src = <<-END_SRC
-
def #{selector}(field, options = {})
-
label_for_field(field, options) + super(field, options.except(:label)).html_safe
-
end
-
END_SRC
-
14
class_eval src, __FILE__, __LINE__
-
end
-
-
1
def select(field, choices, options = {}, html_options = {})
-
48
label_for_field(field, options) + super(field, choices, options, html_options.except(:label)).html_safe
-
end
-
-
1
def time_zone_select(field, priority_zones = nil, options = {}, html_options = {})
-
label_for_field(field, options) + super(field, priority_zones, options, html_options.except(:label)).html_safe
-
end
-
-
# Returns a label tag for the given field
-
1
def label_for_field(field, options = {})
-
160
return ''.html_safe if options.delete(:no_label)
-
144
text = options[:label].is_a?(Symbol) ? l(options[:label]) : options[:label]
-
144
text ||= l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym)
-
144
text += @template.content_tag("span", " *", :class => "required") if options.delete(:required)
-
144
@template.content_tag("label", text.html_safe,
-
144
:class => (@object && @object.errors[field].present? ? "error" : nil),
-
144
:for => (@object_name.to_s + "_" + field.to_s))
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Views
-
1
module MyPage
-
1
module Block
-
1
def self.additional_blocks
-
@@additional_blocks ||= Dir.glob("#{Redmine::Plugin.directory}/*/app/views/my/blocks/_*.{rhtml,erb}").inject({}) do |h,file|
-
1
name = File.basename(file).split('.').first.gsub(/^_/, '')
-
1
h[name] = name.to_sym
-
1
h
-
1
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Views
-
1
class OtherFormatsBuilder
-
1
def initialize(view)
-
30
@view = view
-
end
-
-
1
def link_to(name, options={})
-
72
url = { :format => name.to_s.downcase }.merge(options.delete(:url) || {}).except('page')
-
72
caption = options.delete(:caption) || name
-
72
html_options = { :class => name.to_s.downcase, :rel => 'nofollow' }.merge(options)
-
72
@view.content_tag('span', @view.link_to(caption, url, html_options))
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'digest/md5'
-
-
1
module Redmine
-
1
module WikiFormatting
-
1
class StaleSectionError < Exception; end
-
-
1
@@formatters = {}
-
-
1
class << self
-
1
def map
-
1
yield self
-
end
-
-
1
def register(name, formatter, helper)
-
1
raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name.to_s]
-
1
@@formatters[name.to_s] = {:formatter => formatter, :helper => helper}
-
end
-
-
1
def formatter
-
2
formatter_for(Setting.text_formatting)
-
end
-
-
1
def formatter_for(name)
-
2087
entry = @@formatters[name.to_s]
-
2087
(entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
-
end
-
-
1
def helper_for(name)
-
14
entry = @@formatters[name.to_s]
-
14
(entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper
-
end
-
-
1
def format_names
-
@@formatters.keys.map
-
end
-
-
1
def to_html(format, text, options = {})
-
2085
text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, text, options[:object], options[:attribute])
-
# Text retrieved from the cache store may be frozen
-
# We need to dup it so we can do in-place substitutions with gsub!
-
cache_store.fetch cache_key do
-
formatter_for(format).new(text).to_html
-
end.dup
-
else
-
2085
formatter_for(format).new(text).to_html
-
end
-
2085
text
-
end
-
-
# Returns true if the text formatter supports single section edit
-
1
def supports_section_edit?
-
2
(formatter.instance_methods & ['update_section', :update_section]).any?
-
end
-
-
# Returns a cache key for the given text +format+, +text+, +object+ and +attribute+ or nil if no caching should be done
-
1
def cache_key_for(format, text, object, attribute)
-
if object && attribute && !object.new_record? && format.present?
-
"formatted_text/#{format}/#{object.class.model_name.cache_key}/#{object.id}-#{attribute}-#{Digest::MD5.hexdigest text}"
-
end
-
end
-
-
# Returns the cache store used to cache HTML output
-
1
def cache_store
-
ActionController::Base.cache_store
-
end
-
end
-
-
1
module LinksHelper
-
AUTO_LINK_RE = %r{
-
( # leading text
-
<\w+.*?>| # leading HTML tag, or
-
[^=<>!:'"/]| # leading punctuation, or
-
^ # beginning of line
-
)
-
(
-
(?:https?://)| # protocol spec, or
-
(?:s?ftps?://)|
-
(?:www\.) # www.*
-
)
-
(
-
(\S+?) # url
-
(\/)? # slash
-
)
-
((?:>)?|[^[:alnum:]_\=\/;\(\)]*?) # post
-
(?=<|\s|$)
-
1
}x unless const_defined?(:AUTO_LINK_RE)
-
-
# Destructively remplaces urls into clickable links
-
1
def auto_link!(text)
-
2085
text.gsub!(AUTO_LINK_RE) do
-
1003
all, leading, proto, url, post = $&, $1, $2, $3, $6
-
1003
if leading =~ /<a\s/i || leading =~ /![<>=]?/
-
# don't replace URL's that are already linked
-
# and URL's prefixed with ! !> !< != (textile images)
-
all
-
else
-
# Idea below : an URL with unbalanced parethesis and
-
# ending by ')' is put into external parenthesis
-
1003
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
-
url=url[0..-2] # discard closing parenth from url
-
post = ")"+post # add closing parenth to post
-
end
-
1003
content = proto + url
-
1003
href = "#{proto=="www."?"http://www.":proto}#{url}"
-
1003
%(#{leading}<a class="external" href="#{ERB::Util.html_escape href}">#{ERB::Util.html_escape content}</a>#{post}).html_safe
-
end
-
end
-
end
-
-
# Destructively remplaces email addresses into clickable links
-
1
def auto_mailto!(text)
-
2085
text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
-
mail = $1
-
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
-
mail
-
else
-
%(<a class="email" href="mailto:#{ERB::Util.html_escape mail}">#{ERB::Util.html_escape mail}</a>).html_safe
-
end
-
end
-
end
-
end
-
-
# Default formatter module
-
1
module NullFormatter
-
1
class Formatter
-
1
include ActionView::Helpers::TagHelper
-
1
include ActionView::Helpers::TextHelper
-
1
include ActionView::Helpers::UrlHelper
-
1
include Redmine::WikiFormatting::LinksHelper
-
-
1
def initialize(text)
-
@text = text
-
end
-
-
1
def to_html(*args)
-
t = CGI::escapeHTML(@text)
-
auto_link!(t)
-
auto_mailto!(t)
-
simple_format(t, {}, :sanitize => false)
-
end
-
end
-
-
1
module Helper
-
1
def wikitoolbar_for(field_id)
-
end
-
-
1
def heads_for_wiki_formatter
-
end
-
-
1
def initial_page_content(page)
-
page.pretty_title.to_s
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module WikiFormatting
-
1
module Macros
-
1
module Definitions
-
# Returns true if +name+ is the name of an existing macro
-
1
def macro_exists?(name)
-
Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym)
-
end
-
-
1
def exec_macro(name, obj, args, text)
-
macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym]
-
return unless macro_options
-
-
method_name = "macro_#{name}"
-
unless macro_options[:parse_args] == false
-
args = args.split(',').map(&:strip)
-
end
-
-
begin
-
if self.class.instance_method(method_name).arity == 3
-
send(method_name, obj, args, text)
-
elsif text
-
raise "This macro does not accept a block of text"
-
else
-
send(method_name, obj, args)
-
end
-
rescue => e
-
"<div class=\"flash error\">Error executing the <strong>#{h name}</strong> macro (#{h e.to_s})</div>".html_safe
-
end
-
end
-
-
1
def extract_macro_options(args, *keys)
-
options = {}
-
while args.last.to_s.strip =~ %r{^(.+?)\=(.+)$} && keys.include?($1.downcase.to_sym)
-
options[$1.downcase.to_sym] = $2
-
args.pop
-
end
-
return [args, options]
-
end
-
end
-
-
1
@@available_macros = {}
-
1
mattr_accessor :available_macros
-
-
1
class << self
-
# Plugins can use this method to define new macros:
-
#
-
# Redmine::WikiFormatting::Macros.register do
-
# desc "This is my macro"
-
# macro :my_macro do |obj, args|
-
# "My macro output"
-
# end
-
#
-
# desc "This is my macro that accepts a block of text"
-
# macro :my_macro do |obj, args, text|
-
# "My macro output"
-
# end
-
# end
-
1
def register(&block)
-
class_eval(&block) if block_given?
-
end
-
-
# Defines a new macro with the given name, options and block.
-
#
-
# Options:
-
# * :desc - A description of the macro
-
# * :parse_args => false - Disables arguments parsing (the whole arguments
-
# string is passed to the macro)
-
#
-
# Macro blocks accept 2 or 3 arguments:
-
# * obj: the object that is rendered (eg. an Issue, a WikiContent...)
-
# * args: macro arguments
-
# * text: the block of text given to the macro (should be present only if the
-
# macro accepts a block of text). text is a String or nil if the macro is
-
# invoked without a block of text.
-
#
-
# Examples:
-
# By default, when the macro is invoked, the coma separated list of arguments
-
# is split and passed to the macro block as an array. If no argument is given
-
# the macro will be invoked with an empty array:
-
#
-
# macro :my_macro do |obj, args|
-
# # args is an array
-
# # and this macro do not accept a block of text
-
# end
-
#
-
# You can disable arguments spliting with the :parse_args => false option. In
-
# this case, the full string of arguments is passed to the macro:
-
#
-
# macro :my_macro, :parse_args => false do |obj, args|
-
# # args is a string
-
# end
-
#
-
# Macro can optionally accept a block of text:
-
#
-
# macro :my_macro do |obj, args, text|
-
# # this macro accepts a block of text
-
# end
-
#
-
# Macros are invoked in formatted text using double curly brackets. Arguments
-
# must be enclosed in parenthesis if any. A new line after the macro name or the
-
# arguments starts the block of text that will be passe to the macro (invoking
-
# a macro that do not accept a block of text with some text will fail).
-
# Examples:
-
#
-
# No arguments:
-
# {{my_macro}}
-
#
-
# With arguments:
-
# {{my_macro(arg1, arg2)}}
-
#
-
# With a block of text:
-
# {{my_macro
-
# multiple lines
-
# of text
-
# }}
-
#
-
# With arguments and a block of text
-
# {{my_macro(arg1, arg2)
-
# multiple lines
-
# of text
-
# }}
-
#
-
# If a block of text is given, the closing tag }} must be at the start of a new line.
-
1
def macro(name, options={}, &block)
-
6
options.assert_valid_keys(:desc, :parse_args)
-
6
unless name.to_s.match(/\A\w+\z/)
-
raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)"
-
end
-
6
unless block_given?
-
raise "Can not create a macro without a block!"
-
end
-
6
name = name.to_s.downcase.to_sym
-
6
available_macros[name] = {:desc => @@desc || ''}.merge(options)
-
6
@@desc = nil
-
6
Definitions.send :define_method, "macro_#{name}", &block
-
end
-
-
# Sets description for the next macro to be defined
-
1
def desc(txt)
-
6
@@desc = txt
-
end
-
end
-
-
# Builtin macros
-
1
desc "Sample macro."
-
1
macro :hello_world do |obj, args, text|
-
h("Hello world! Object: #{obj.class.name}, " +
-
(args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") +
-
" and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.")
-
)
-
end
-
-
1
desc "Displays a list of all available macros, including description if available."
-
1
macro :macro_list do |obj, args|
-
out = ''.html_safe
-
@@available_macros.each do |macro, options|
-
out << content_tag('dt', content_tag('code', macro.to_s))
-
out << content_tag('dd', textilizable(options[:desc]))
-
end
-
content_tag('dl', out)
-
end
-
-
desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
-
" !{{child_pages}} -- can be used from a wiki page only\n" +
-
1
" !{{child_pages(depth=2)}} -- display 2 levels nesting only\n"
-
" !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
-
1
" !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
-
1
macro :child_pages do |obj, args|
-
args, options = extract_macro_options(args, :parent, :depth)
-
options[:depth] = options[:depth].to_i if options[:depth].present?
-
-
page = nil
-
if args.size > 0
-
page = Wiki.find_page(args.first.to_s, :project => @project)
-
elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
-
page = obj.page
-
else
-
raise 'With no argument, this macro can be called from wiki pages only.'
-
end
-
raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
-
pages = page.self_and_descendants(options[:depth]).group_by(&:parent_id)
-
render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
-
end
-
-
1
desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
-
1
macro :include do |obj, args|
-
page = Wiki.find_page(args.first.to_s, :project => @project)
-
raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
-
@included_wiki_pages ||= []
-
raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
-
@included_wiki_pages << page.title
-
out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
-
@included_wiki_pages.pop
-
out
-
end
-
-
1
desc "Inserts of collapsed block of text. Example:\n\n {{collapse(View details...)\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}"
-
1
macro :collapse do |obj, args, text|
-
html_id = "collapse-#{Redmine::Utils.random_hex(4)}"
-
show_label = args[0] || l(:button_show)
-
hide_label = args[1] || args[0] || l(:button_hide)
-
js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
-
out = ''.html_safe
-
out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed')
-
out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;')
-
out << content_tag('div', textilizable(text, :object => obj), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
-
out
-
end
-
-
1
desc "Displays a clickable thumbnail of an attached image. Examples:\n\n<pre>{{thumbnail(image.png)}}\n{{thumbnail(image.png, size=300, title=Thumbnail)}}</pre>"
-
1
macro :thumbnail do |obj, args|
-
args, options = extract_macro_options(args, :size, :title)
-
filename = args.first
-
raise 'Filename required' unless filename.present?
-
size = options[:size]
-
raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
-
size = size.to_i
-
size = nil unless size > 0
-
if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
-
title = options[:title] || attachment.title
-
img = image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size), :alt => attachment.filename)
-
link_to(img, url_for(:controller => 'attachments', :action => 'show', :id => attachment), :class => 'thumbnail', :title => title)
-
else
-
raise "Attachment #{filename} not found"
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redcloth3'
-
1
require 'digest/md5'
-
-
1
module Redmine
-
1
module WikiFormatting
-
1
module Textile
-
1
class Formatter < RedCloth3
-
1
include ActionView::Helpers::TagHelper
-
1
include Redmine::WikiFormatting::LinksHelper
-
-
1
alias :inline_auto_link :auto_link!
-
1
alias :inline_auto_mailto :auto_mailto!
-
-
# auto_link rule after textile rules so that it doesn't break !image_url! tags
-
1
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto]
-
-
1
def initialize(*args)
-
2085
super
-
2085
self.hard_breaks=true
-
2085
self.no_span_caps=true
-
2085
self.filter_styles=false
-
end
-
-
1
def to_html(*rules)
-
2085
@toc = []
-
2085
super(*RULES).to_s
-
end
-
-
1
def get_section(index)
-
section = extract_sections(index)[1]
-
hash = Digest::MD5.hexdigest(section)
-
return section, hash
-
end
-
-
1
def update_section(index, update, hash=nil)
-
t = extract_sections(index)
-
if hash.present? && hash != Digest::MD5.hexdigest(t[1])
-
raise Redmine::WikiFormatting::StaleSectionError
-
end
-
t[1] = update unless t[1].blank?
-
t.reject(&:blank?).join "\n\n"
-
end
-
-
1
def extract_sections(index)
-
@pre_list = []
-
text = self.dup
-
rip_offtags text, false, false
-
before = ''
-
s = ''
-
after = ''
-
i = 0
-
l = 1
-
started = false
-
ended = false
-
text.scan(/(((?:.*?)(\A|\r?\n\s*\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))?[ \t](.*?)$)|.*)/m).each do |all, content, lf, heading, level|
-
if heading.nil?
-
if ended
-
after << all
-
elsif started
-
s << all
-
else
-
before << all
-
end
-
break
-
end
-
i += 1
-
if ended
-
after << all
-
elsif i == index
-
l = level.to_i
-
before << content
-
s << heading
-
started = true
-
elsif i > index
-
s << content
-
if level.to_i > l
-
s << heading
-
else
-
after << heading
-
ended = true
-
end
-
else
-
before << all
-
end
-
end
-
sections = [before.strip, s.strip, after.strip]
-
sections.each {|section| smooth_offtags_without_code_highlighting section}
-
sections
-
end
-
-
1
private
-
-
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
-
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
-
1
def hard_break( text )
-
2085
text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
-
end
-
-
1
alias :smooth_offtags_without_code_highlighting :smooth_offtags
-
# Patch to add code highlighting support to RedCloth
-
1
def smooth_offtags( text )
-
2085
unless @pre_list.empty?
-
## replace <pre> content
-
text.gsub!(/<redpre#(\d+)>/) do
-
content = @pre_list[$1.to_i]
-
if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
-
content = "<code class=\"#{$1} syntaxhl\">" +
-
Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
-
end
-
content
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module WikiFormatting
-
1
module Textile
-
1
module Helper
-
1
def wikitoolbar_for(field_id)
-
17
heads_for_wiki_formatter
-
# Is there a simple way to link to a public resource?
-
17
url = "#{Redmine::Utils.relative_url_root}/help/wiki_syntax.html"
-
17
help_link = link_to(l(:setting_text_formatting), url,
-
:onclick => "window.open(\"#{ url }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
-
-
17
javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript help_link}'); wikiToolbar.draw();")
-
end
-
-
1
def initial_page_content(page)
-
"h1. #{@page.pretty_title}"
-
end
-
-
1
def heads_for_wiki_formatter
-
17
unless @heads_for_wiki_formatter_included
-
14
content_for :header_tags do
-
javascript_include_tag('jstoolbar/jstoolbar-textile.min') +
-
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") +
-
14
stylesheet_link_tag('jstoolbar')
-
end
-
14
@heads_for_wiki_formatter_included = true
-
end
-
end
-
end
-
end
-
end
-
end
-
1
class RbAllProjectsController < ApplicationController
-
1
unloadable
-
-
1
before_filter :authorize_global
-
-
1
def statistics
-
1
backlogs_projects = RbCommonHelper.find_backlogs_enabled_active_projects
-
1
@projects = []
-
1
backlogs_projects.each{|p|
-
1
@projects << p unless p.visible?.nil? || p.rb_project_settings.show_in_scrum_stats == false
-
}
-
1
@projects.sort! {|a, b| a.scrum_statistics.score <=> b.scrum_statistics.score}
-
end
-
-
end
-
# Base class of all controllers in Redmine Backlogs
-
1
class RbApplicationController < ApplicationController
-
1
unloadable
-
-
1
before_filter :load_project, :authorize, :check_if_plugin_is_configured
-
-
#provide list of javascript_include_tags which must be rendered before common.js
-
1
def rb_jquery_plugins
-
115
@rb_jquery_plugins
-
end
-
1
def rb_jquery_plugins=(html)
-
99
@rb_jquery_plugins = html
-
end
-
-
1
private
-
-
# Loads the project to be used by the authorize filter to
-
# determine if User.current has permission to invoke the method in question.
-
1
def load_project
-
283
@project = if params[:sprint_id]
-
92
load_sprint
-
92
@sprint.project
-
elsif params[:release_id] && !params[:release_id].empty?
-
12
load_release
-
12
@release.project
-
elsif params[:project_id]
-
179
Project.find(params[:project_id])
-
else
-
raise "Cannot determine project (#{params.inspect})"
-
end
-
end
-
-
1
def check_if_plugin_is_configured
-
352
@settings = Backlogs.settings
-
352
if @settings[:story_trackers].blank? || @settings[:task_tracker].blank?
-
respond_to do |format|
-
format.html { render :file => "shared/not_configured" }
-
end
-
end
-
end
-
-
1
def load_sprint
-
92
@sprint = RbSprint.find(params[:sprint_id])
-
end
-
-
1
def load_release
-
12
@release = RbRelease.find(params[:release_id])
-
end
-
end
-
1
include RbCommonHelper
-
-
1
class RbBurndownChartsController < RbApplicationController
-
1
unloadable
-
-
1
def show
-
4
respond_to do |format|
-
4
format.html
-
end
-
end
-
-
1
def embedded
-
respond_to do |format|
-
format.html { render :template => 'rb_burndown_charts/show.html.erb', :layout => false }
-
end
-
end
-
-
1
def print
-
@width = Backlogs.setting[:burndown_print_width].to_s
-
@height = Backlogs.setting[:burndown_print_height].to_s
-
if @width.blank? || @height.blank?
-
@width = '1300'
-
@height = '600'
-
end
-
respond_to do |format|
-
format.html { render :layout => false }
-
end
-
end
-
-
end
-
1
require 'icalendar'
-
-
1
class RbCalendarsController < RbApplicationController
-
1
unloadable
-
-
1
case Backlogs.platform
-
when :redmine
-
1
before_filter :require_admin_or_api_request, :only => :ical
-
1
accept_api_auth :ical
-
when :chiliproject
-
accept_key_auth :ical
-
end
-
-
1
def ical
-
1
respond_to do |format|
-
2
format.xml { send_data(generate_ical, :disposition => 'attachment') }
-
end
-
end
-
-
1
private
-
-
1
def generate_ical
-
1
cal = Icalendar::Calendar.new
-
-
# current + future sprints
-
1
RbSprint.find(:all, :conditions => ["not sprint_start_date is null and not effective_date is null and project_id = ? and effective_date >= ?", @project.id, Date.today]).each {|sprint|
-
1
summary_text = l(:event_sprint_summary, { :project => @project.name, :summary => sprint.name } )
-
1
description_text = "#{sprint.name}: #{url_for(:controller => 'rb_queries', :only_path => false, :action => 'show', :project_id => @project.id, :sprint_id => sprint.id)}\n#{sprint.description}"
-
-
1
cal.event do
-
1
dtstart sprint.sprint_start_date
-
1
dtend sprint.effective_date
-
1
summary summary_text
-
1
description description_text
-
1
klass 'PRIVATE'
-
1
transp 'TRANSPARENT'
-
end
-
}
-
-
1
open_issues = %Q[
-
#{IssueStatus.table_name}.is_closed = ?
-
and tracker_id in (?)
-
and fixed_version_id in (
-
select id
-
from versions
-
where project_id = ?
-
and status = 'open'
-
and not sprint_start_date is null
-
and effective_date >= ?
-
)
-
]
-
1
open_issues_and_impediments = %Q[
-
(assigned_to_id is null or assigned_to_id = ?)
-
and
-
(
-
(#{open_issues})
-
or
-
( #{IssueStatus.table_name}.is_closed = ?
-
and #{Issue.table_name}.id in (
-
select issue_from_id
-
from issue_relations
-
join issues on issues.id = issue_to_id and relation_type = 'blocks'
-
where #{open_issues})
-
)
-
)
-
]
-
-
1
conditions = [open_issues_and_impediments]
-
# me or none
-
1
conditions << User.current.id
-
-
# open stories/tasks
-
1
conditions << false
-
1
conditions << RbStory.trackers + [RbTask.tracker]
-
1
conditions << @project.id
-
1
conditions << Date.today
-
-
# open impediments...
-
1
conditions << false
-
-
# ... for open stories/tasks
-
1
conditions << false
-
1
conditions << RbStory.trackers + [RbTask.tracker]
-
1
conditions << @project.id
-
1
conditions << Date.today
-
-
1
issues = Issue.find(:all, :include => :status, :conditions => conditions).each {|issue|
-
1
summary_text = l(:todo_issue_summary, { :type => issue.tracker.name, :summary => issue.subject } )
-
1
description_text = "#{issue.subject}: #{url_for(:controller => 'issues', :only_path => false, :action => 'show', :id => issue.id)}\n#{issue.description}"
-
# I know this should be "cal.todo do", but outlook in it's
-
# infinite stupidity doesn't support VTODO
-
1
cal.event do
-
1
summary summary_text
-
1
description description_text
-
1
dtstart Date.today
-
1
dtend (Date.today + 1)
-
1
klass 'PRIVATE'
-
1
transp 'TRANSPARENT'
-
end
-
}
-
-
1
cal.to_ical
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbHooksRenderController < RbApplicationController
-
1
unloadable
-
-
1
def view_issues_sidebar
-
15
locals = {
-
:sprints => RbSprint.open_sprints(@project),
-
:project => @project,
-
:sprint => @sprint,
-
15
:webcal => (request.ssl? ? 'webcals' : 'webcal'),
-
:key => User.current.api_key
-
}
-
-
15
respond_to do |format|
-
30
format.html { render :template => 'backlogs/view_issues_sidebar.html.erb', :layout => false, :locals => locals }
-
end
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbImpedimentsController < RbApplicationController
-
1
unloadable
-
-
1
def create
-
2
@settings = Backlogs.settings
-
2
begin
-
2
@impediment = RbTask.create_with_relationships(params, User.current.id, @project.id, true)
-
rescue => e
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
2
result = @impediment.errors.size
-
2
status = (result == 0 ? 200 : 400)
-
2
@include_meta = true
-
-
2
respond_to do |format|
-
4
format.html { render :partial => "impediment", :object => @impediment, :status => status }
-
end
-
end
-
-
1
def update
-
1
@impediment = RbTask.find_by_id(params[:id])
-
1
@settings = Backlogs.settings
-
1
begin
-
1
result = @impediment.update_with_relationships(params)
-
rescue => e
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
1
status = (result ? 200 : 400)
-
1
@include_meta = true
-
-
1
respond_to do |format|
-
2
format.html { render :partial => "impediment", :object => @impediment, :status => status }
-
end
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbMasterBacklogsController < RbApplicationController
-
1
unloadable
-
-
1
def show
-
55
product_backlog_stories = RbStory.product_backlog(@project)
-
55
@product_backlog = { :sprint => nil, :stories => product_backlog_stories }
-
-
#collect all sprints which are sharing into @project
-
55
sprints = @project.open_shared_sprints
-
55
@sprint_backlogs = RbStory.backlogs_by_sprint(@project, sprints)
-
-
55
releases = @project.open_releases_by_date
-
55
@release_backlogs = RbStory.backlogs_by_release(@project, releases)
-
-
55
@last_update = [product_backlog_stories,
-
179
@sprint_backlogs.map{|s| s[:stories]},
-
4
@release_backlogs.map{|r| r[:releases]}
-
306
].flatten.compact.map{|s| s.updated_on}.sort.last
-
-
55
respond_to do |format|
-
110
format.html { render :layout => "rb"}
-
end
-
end
-
-
1
def _menu_new
-
18
links = []
-
18
label_new = :label_new_story
-
18
add_class = 'add_new_story'
-
-
18
if @settings[:sharing_enabled]
-
# FIXME: (pa sharing) usability is bad, menu is inconsistent. Sometimes we have a submenu with one entry, sometimes we have non-sharing behavior without submenu
-
18
if @sprint #menu for sprint
-
15
return [] unless @sprint.status == 'open' #closed/locked versions are not assignable versions
-
15
projects = @sprint.shared_to_projects(@project)
-
elsif @release #menu for release
-
projects = @release.shared_to_projects(@project)
-
else #menu for product backlog
-
3
projects = @project.projects_in_shared_product_backlog
-
end
-
#make the submenu or single link
-
18
if !projects.empty?
-
18
if projects.length > 1
-
14
links << {:label => l(label_new), :url => '#', :sub => []}
-
14
projects.each{|project|
-
64
links.first[:sub] << {:label => project.name, :url => '#', :classname => "#{add_class} project_id_#{project.id}"}
-
}
-
else
-
4
links << {:label => l(label_new), :url => '#', :classname => "#{add_class} project_id_#{projects[0].id}"}
-
end
-
end
-
else #no sharing, only own project in the menu
-
links << {:label => l(label_new), :url => '#', :classname => add_class}
-
end
-
18
return links
-
end
-
-
1
def menu
-
23
links = []
-
-
23
links += _menu_new if User.current.allowed_to?(:create_stories, @project)
-
-
links << {:label => l(:label_new_sprint), :url => '#', :classname => 'add_new_sprint'
-
23
} unless @sprint || !User.current.allowed_to?(:create_sprints, @project)
-
links << {:label => l(:label_task_board),
-
:url => url_for(:controller => 'rb_taskboards', :action => 'show', :sprint_id => @sprint, :only_path => true)
-
23
} if @sprint && @sprint.stories.size > 0 && Backlogs.task_workflow(@project) && User.current.allowed_to?(:view_taskboards, @project)
-
links << {:label => l(:label_burndown),
-
:url => '#',
-
:classname => 'show_burndown_chart'
-
23
} if @sprint && @sprint.stories.size > 0 && @sprint.has_burndown?
-
links << {:label => l(:label_stories_tasks),
-
:url => url_for(:controller => 'rb_queries', :action => 'show', :project_id => @project.id, :sprint_id => @sprint, :only_path => true)
-
23
} if @sprint && @sprint.stories.size > 0
-
links << {:label => l(:label_stories),
-
:url => url_for(:controller => 'rb_queries', :action => 'show', :project_id => @project, :only_path => true)
-
23
} unless @sprint || @release
-
links << {:label => l(:label_sprint_cards),
-
:url => url_for(:controller => 'rb_stories', :action => 'index', :project_id => @project.identifier, :sprint_id => @sprint, :format => 'pdf', :only_path => true)
-
23
} if @sprint && BacklogsPrintableCards::CardPageLayout.selected && @sprint.stories.size > 0
-
links << {:label => l(:label_product_cards),
-
:url => url_for(:controller => 'rb_stories', :action => 'index', :project_id => @project.identifier, :format => 'pdf', :only_path => true)
-
23
} unless @sprint || @release
-
links << {:label => l(:label_wiki),
-
:url => url_for(:controller => 'rb_wikis', :action => 'show', :sprint_id => @sprint, :only_path => true)
-
131
} if @sprint && @project.enabled_modules.any? {|m| m.name=="wiki" }
-
links << {:label => l(:label_download_sprint),
-
:url => url_for(:controller => 'rb_sprints', :action => 'download', :sprint_id => @sprint, :format => 'xml', :only_path => true)
-
23
} if @sprint && @sprint.has_burndown?
-
links << {:label => l(:label_reset),
-
:url => url_for(:controller => 'rb_sprints', :action => 'reset', :sprint_id => @sprint, :only_path => true),
-
:warning => view_context().escape_javascript(l(:warning_reset_sprint)).gsub(/\/n/, "\n")
-
23
} if @sprint && @sprint.sprint_start_date && User.current.allowed_to?(:reset_sprint, @project)
-
links << {:label => l(:label_version),
-
:url => url_for(:controller => 'versions', :action => 'show', :id => @sprint, :target => '_blank', :only_path => true)
-
23
} if @sprint
-
links << {:label => l(:label_release),
-
:url => url_for(:controller => 'rb_releases', :action => 'show', :release_id => @release, :target => '_blank', :only_path => true)
-
23
} if @release
-
-
-
23
respond_to do |format|
-
46
format.html { render :json => links }
-
end
-
end
-
-
1
if Rails::VERSION::MAJOR < 3
-
def view_context
-
@template
-
end
-
end
-
-
1
def closed_sprints
-
1
c_sprints = @project.closed_shared_sprints
-
1
@backlogs = RbStory.backlogs_by_sprint(@project, c_sprints)
-
1
respond_to do |format|
-
2
format.html { render :partial => 'closedbacklog', :collection => @backlogs }
-
end
-
end
-
-
end
-
1
include RbCommonHelper
-
1
include ProjectsHelper
-
-
1
class RbProjectSettingsController < RbApplicationController
-
1
unloadable
-
-
1
def project_settings
-
3
enabled = false
-
3
enabled_scrum_stats = false
-
3
if request.post? and params[:settings]
-
3
enabled = true if params[:settings]["show_stories_from_subprojects"]=="enabled"
-
3
enabled_scrum_stats = true if params[:settings]["show_in_scrum_stats"]=="enabled"
-
end
-
3
settings = @project.rb_project_settings
-
3
settings.show_stories_from_subprojects = enabled
-
3
settings.show_in_scrum_stats = enabled_scrum_stats
-
3
if settings.save
-
3
flash[:notice] = t(:rb_project_settings_updated)
-
else
-
flash[:error] = t(:rb_project_settings_update_error)
-
end
-
redirect_to :controller => 'projects', :action => 'settings', :id => @project,
-
3
:tab => 'backlogs'
-
end
-
-
end
-
1
class RbQueriesController < RbApplicationController
-
1
unloadable
-
-
1
def show
-
9
@query = Query.new(:name => "_")
-
9
@query.project = @project
-
-
9
if params[:sprint_id]
-
6
@query.add_filter("status_id", '*', ['']) # All statuses
-
6
@query.add_filter("fixed_version_id", '=', [params[:sprint_id]])
-
6
@query.add_filter("backlogs_issue_type", '=', ['any'])
-
else
-
3
@query.add_filter("status_id", 'o', ['']) # only open
-
3
@query.add_filter("fixed_version_id", '!*', ['']) # only unassigned
-
3
@query.add_filter("backlogs_issue_type", '=', ['story'])
-
end
-
-
63
column_names = @query.columns.collect{|col| col.name}
-
9
column_names = column_names + ['position'] unless column_names.include?('position')
-
-
9
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :column_names => column_names}
-
9
redirect_to :controller => 'issues', :action => 'index', :project_id => @project.id, :sort => 'position'
-
end
-
-
1
def impediments
-
1
@query = Query.new(:name => "_")
-
1
@query.project = @project
-
1
@query.add_filter("status_id", 'o', ['']) # only open
-
1
@query.add_filter("fixed_version_id", '=', [params[:sprint_id]])
-
1
@query.add_filter("backlogs_issue_type", '=', ['impediment'])
-
1
session[:query] = {:project_id => @query.project_id, :filters => @query.filters }
-
1
redirect_to :controller => 'issues', :action => 'index', :project_id => @project.id
-
end
-
end
-
1
include RbCommonHelper
-
1
include RbFormHelper
-
1
include ProjectsHelper
-
-
# Responsible for exposing release CRUD.
-
1
class RbReleasesController < RbApplicationController
-
1
unloadable
-
-
1
def index
-
9
@releases = @project.releases
-
end
-
-
1
def show
-
6
@remaining_story_points = @release.remaining_story_points
-
-
6
respond_to do |format|
-
12
format.html { render }
-
6
format.csv { send_data(release_burndown_to_csv(@release), :type => 'text/csv; header=present', :filename => 'export.csv') }
-
end
-
end
-
-
1
def new
-
2
@release = RbRelease.new(:project => @project)
-
2
if request.post?
-
1
@release.attributes = params[:release]
-
1
if @release.save
-
1
flash[:notice] = l(:notice_successful_create)
-
1
redirect_to :action => 'index', :project_id => @project
-
end
-
end
-
end
-
-
1
def edit
-
4
if request.post? and @release.update_attributes(params[:release])
-
2
flash[:notice] = l(:notice_successful_update)
-
2
redirect_to :controller => 'rb_releases', :action => 'show', :release_id => @release
-
# else
-
# flash[:notice] = l(:notice_unsuccessful_update)
-
end
-
end
-
-
1
def update
-
except = ['id', 'project_id']
-
attribs = params.select{|k,v| (!except.include? k) and (RbRelease.column_names.include? k) }
-
attribs = Hash[*attribs.flatten]
-
begin
-
result = @release.update_attributes attribs
-
rescue => e
-
Rails.logger.debug e
-
Rails.logger.debug e.backtrace.join("\n")
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
respond_to do |format|
-
format.html { render :partial => "release_mbp", :status => (result ? 200 : 400), :locals => { :release => @release, :cls => 'model release' } }
-
end
-
end
-
-
1
def destroy
-
1
@release.destroy
-
1
redirect_to :controller => 'rb_releases', :action => 'index', :project_id => @project
-
end
-
-
end
-
1
class RbServerVariablesController < RbApplicationController
-
1
unloadable
-
-
# for index there's no @project
-
# (eliminates the need of RbAllProjectsController)
-
1
skip_before_filter :load_project, :authorize, :only => [:index]
-
-
1
def index
-
90
@context = params[:context]
-
90
respond_to do |format|
-
90
format.html { render_404 }
-
180
format.js { render :file => 'rb_server_variables/show.js.erb', :layout => false }
-
end
-
end
-
-
1
alias :project :index
-
1
alias :sprint :index
-
end
-
1
include RbCommonHelper
-
-
# Responsible for exposing sprint CRUD. It SHOULD NOT be used
-
# for displaying the taskboard since the taskboard is a management
-
# interface used for managing objects within a sprint. For
-
# info about the taskboard, see RbTaskboardsController
-
1
class RbSprintsController < RbApplicationController
-
1
unloadable
-
-
1
def create
-
28
attribs = params.select{|k,v| k != 'id' and RbSprint.column_names.include? k }
-
2
attribs = Hash[*attribs.flatten]
-
2
@sprint = RbSprint.new(attribs)
-
-
#share the sprint according to the global setting
-
2
default_sharing = Backlogs.setting[:sharing_new_sprint_sharingmode]
-
2
if default_sharing
-
1
if @sprint.allowed_sharings.include? default_sharing
-
1
@sprint.sharing = default_sharing
-
end
-
end
-
-
2
begin
-
2
@sprint.save!
-
rescue => e
-
Rails.logger.debug e
-
Rails.logger.debug e.backtrace.join("\n")
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
2
result = @sprint.errors.size
-
2
status = (result == 0 ? 200 : 400)
-
-
2
respond_to do |format|
-
4
format.html { render :partial => "sprint", :status => status, :locals => { :sprint => @sprint, :cls => 'model sprint' } }
-
end
-
end
-
-
1
def update
-
4
except = ['id', 'project_id']
-
58
attribs = params.select{|k,v| (!except.include? k) and (RbSprint.column_names.include? k) }
-
4
attribs = Hash[*attribs.flatten]
-
4
begin
-
4
result = @sprint.update_attributes attribs
-
rescue => e
-
Rails.logger.debug e
-
Rails.logger.debug e.backtrace.join("\n")
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
4
respond_to do |format|
-
8
format.html { render :partial => "sprint", :status => (result ? 200 : 400), :locals => { :sprint => @sprint, :cls => 'model sprint' } }
-
end
-
end
-
-
1
def download
-
bold = {:font => {:bold => true}}
-
dump = BacklogsSpreadsheet::WorkBook.new
-
ws = dump[@sprint.name]
-
ws << [nil, @sprint.id, nil, nil, {:value => @sprint.name, :style => bold}, {:value => 'Start', :style => bold}] + @sprint.days(:all).collect{|d| {:value => d, :style => bold} }
-
bd = @sprint.burndown
-
bd.series(false).sort{|a, b| l("label_#{a}") <=> l("label_#{b}")}.each{ |k|
-
ws << [ nil, nil, nil, nil, l("label_#{k}") ] + bd[k]
-
}
-
-
@sprint.stories.each{|s|
-
ws << [s.tracker.name, s.id, nil, nil, {:value => s.subject, :style => bold}]
-
bd = s.burndown
-
bd.keys.sort{|a, b| l("label_#{a}") <=> l("label_#{b}")}.each{ |k|
-
next if k == :status
-
label = l("label_#{k}")
-
label = {:value => label, :comment => k.to_s} if [:points, :points_accepted].include?(k)
-
ws << [nil, nil, nil, nil, label ] + bd[k]
-
}
-
s.tasks.each {|t|
-
ws << [nil, nil, t.tracker.name, t.id, {:value => t.subject, :style => bold}] + t.burndown
-
}
-
}
-
-
send_data(dump.to_xml, :disposition => 'attachment', :type => 'application/vnd.ms-excel', :filename => "#{@project.identifier}-#{@sprint.name.gsub(/[^a-z0-9]/i, '')}.xml")
-
end
-
-
1
def reset
-
unless @sprint.sprint_start_date
-
render :text => 'Sprint without start date cannot be reset', :status => 400
-
return
-
end
-
-
ids = []
-
status = IssueStatus.default.id
-
Issue.find(:all, :conditions => ['fixed_version_id = ?', @sprint.id]).each {|issue|
-
ids << issue.id.to_s
-
issue.update_attributes!(:created_on => @sprint.sprint_start_date.to_time, :status_id => status)
-
}
-
if ids.size != 0
-
ids = ids.join(',')
-
Issue.connection.execute("update issues set updated_on = created_on where id in (#{ids})")
-
-
Journal.connection.execute("delete from journal_details where journal_id in (select id from journals where journalized_type = 'Issue' and journalized_id in (#{ids}))")
-
Journal.connection.execute("delete from journals where (notes is null or notes = '') and journalized_type = 'Issue' and journalized_id in (#{ids})")
-
Journal.connection.execute("update journals
-
set created_on = (select created_on
-
from issues
-
where journalized_id = issues.id)
-
where journalized_type = 'Issue' and journalized_id in (#{ids})")
-
end
-
-
redirect_to :controller => 'rb_master_backlogs', :action => 'show', :project_id => @project.identifier
-
end
-
-
1
def close_completed
-
@project.close_completed_versions
-
-
redirect_to :controller => 'rb_master_backlogs', :action => 'show', :project_id => @project
-
end
-
end
-
1
require 'prawn'
-
1
require 'backlogs_printable_cards'
-
-
1
include RbCommonHelper
-
-
1
class RbStoriesController < RbApplicationController
-
1
unloadable
-
1
include BacklogsPrintableCards
-
-
1
def index
-
2
if ! BacklogsPrintableCards::CardPageLayout.selected
-
render :text => "No label stock selected. How did you get here?", :status => 500
-
return
-
end
-
-
2
begin
-
2
cards = BacklogsPrintableCards::PrintableCards.new(params[:sprint_id] ? @sprint.stories : RbStory.product_backlog(@project), params[:sprint_id], current_language)
-
rescue Prawn::Errors::CannotFit
-
render :text => "There was a problem rendering the cards. A possible error could be that the selected font exceeds a render box", :status => 500
-
return
-
end
-
-
2
respond_to do |format|
-
2
format.pdf {
-
2
send_data(cards.pdf.render, :disposition => 'attachment', :type => 'application/pdf')
-
}
-
end
-
end
-
-
1
def create
-
1
params['author_id'] = User.current.id
-
1
begin
-
1
story = RbStory.create_and_position(params)
-
rescue => e
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
1
status = (story.id ? 200 : 400)
-
-
1
respond_to do |format|
-
2
format.html { render :partial => "story", :object => story, :status => status }
-
end
-
end
-
-
1
def update
-
33
story = RbStory.find(params[:id])
-
33
begin
-
33
result = story.update_and_position!(params)
-
rescue => e
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
33
status = (result ? 200 : 400)
-
-
33
respond_to do |format|
-
66
format.html { render :partial => "story", :object => story, :status => status }
-
end
-
end
-
-
1
def tooltip
-
story = RbStory.find(params[:id])
-
respond_to do |format|
-
format.html { render :partial => "tooltip", :object => story }
-
end
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbTaskboardsController < RbApplicationController
-
1
unloadable
-
-
1
def show
-
38
stories = @sprint.stories
-
143
@story_ids = stories.map{|s| s.id}
-
-
38
@settings = Backlogs.settings
-
-
## determine status columns to show
-
38
tracker = Tracker.find_by_id(RbTask.tracker)
-
38
statuses = tracker.issue_statuses
-
# disable columns by default
-
38
if User.current.admin?
-
@statuses = statuses
-
else
-
38
enabled = {}
-
266
statuses.each{|s| enabled[s.id] = false}
-
# enable all statuses held by current tasks, regardless of whether the current user has access
-
263
RbTask.find(:all, :conditions => ['fixed_version_id = ?', @sprint.id]).each {|task| enabled[task.status_id] = true }
-
-
38
roles = User.current.roles_for_project(@project)
-
#@transitions = {}
-
38
statuses.each {|status|
-
-
# enable all statuses the current user can reach from any task status
-
228
[false, true].each {|creator|
-
456
[false, true].each {|assignee|
-
-
5472
allowed = status.new_statuses_allowed_to(roles, tracker, creator, assignee).collect{|s| s.id}
-
#@transitions["c#{creator ? 'y' : 'n'}a#{assignee ? 'y' : 'n'}"] = allowed
-
5472
allowed.each{|s| enabled[s] = true}
-
}
-
}
-
}
-
266
@statuses = statuses.select{|s| enabled[s.id]}
-
end
-
-
38
if @sprint.stories.size == 0
-
1
@last_updated = nil
-
else
-
37
@last_updated = RbTask.find(:first,
-
:conditions => ['tracker_id = ? and fixed_version_id = ?', RbTask.tracker, @sprint.stories[0].fixed_version_id],
-
:order => "updated_on DESC")
-
end
-
-
38
respond_to do |format|
-
76
format.html { render :layout => "rb" }
-
end
-
end
-
-
1
def current
-
sprint = @project.active_sprint
-
if sprint
-
redirect_to :controller => 'rb_taskboards', :action => 'show', :sprint_id => sprint
-
return
-
end
-
respond_to do |format|
-
format.html { redirect_back_or_default(project_url(@project)) }
-
end
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbTasksController < RbApplicationController
-
1
unloadable
-
-
1
def create
-
6
@settings = Backlogs.settings
-
6
@task = nil
-
6
begin
-
6
@task = RbTask.create_with_relationships(params, User.current.id, @project.id)
-
rescue => e
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
6
result = @task.errors.size
-
6
status = (result == 0 ? 200 : 400)
-
6
@include_meta = true
-
-
6
respond_to do |format|
-
12
format.html { render :partial => "task", :object => @task, :status => status }
-
end
-
end
-
-
1
def update
-
26
@task = RbTask.find_by_id(params[:id])
-
26
@settings = Backlogs.settings
-
26
result = @task.update_with_relationships(params)
-
26
status = (result ? 200 : 400)
-
26
@include_meta = true
-
-
26
@task.story.story_follow_task_state if @task.story # && Backlogs.Setting[:story_loosely_follows_tasks_states]
-
-
26
respond_to do |format|
-
52
format.html { render :partial => "task", :object => @task, :status => status }
-
end
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbUpdatedItemsController < RbApplicationController
-
1
unloadable
-
-
# Returns all models that have changed since params[:since]
-
# params[:only] limits the types of models that the method
-
# should return
-
1
def show
-
9
@settings = Backlogs.settings
-
18
only = (params[:only] ? params[:only].split(/, ?/).map{|v| v.to_sym} : [:sprints, :stories, :tasks, :impediments])
-
9
@items = HashWithIndifferentAccess.new
-
9
@include_meta = true
-
9
@last_update = nil
-
-
9
latest_updates = []
-
9
if only.include? :stories
-
6
@items[:stories] = RbStory.find_all_updated_since(params[:since], @project.id)
-
6
if @items[:stories].length > 0
-
36
latest_updates << @items[:stories].sort{ |a,b| a.updated_on <=> b.updated_on }.last
-
end
-
end
-
-
9
if only.include? :tasks
-
1
@items[:tasks] = RbTask.find_all_updated_since(params[:since], @project.id, false, params[:sprint])
-
1
if @items[:tasks].length > 0
-
1
latest_updates << @items[:tasks].sort{ |a,b| a.updated_on <=> b.updated_on }.last
-
end
-
end
-
-
9
if only.include? :impediments
-
2
@items[:impediments] = RbTask.find_all_updated_since(params[:since], @project.id, true, params[:sprint])
-
2
if @items[:impediments].length > 0
-
2
latest_updates << @items[:impediments].sort{ |a,b| a.updated_on <=> b.updated_on }.last
-
end
-
end
-
-
9
if latest_updates.length > 0
-
8
@last_update = latest_updates.sort{ |a,b| a.updated_on <=> b.updated_on }.last.updated_on
-
end
-
-
9
respond_to do |format|
-
18
format.html { render :layout => false }
-
end
-
end
-
end
-
1
class RbWikisController < RbApplicationController
-
1
unloadable
-
-
# NOTE: This method is public (see init.rb). We will let Redmine core's
-
# WikiController#index tak care of autorization
-
# NOTE: this redirection causes a page to be created from a template
-
# as a side-effect of calling @sprint.wiki_page. See rb_sprint model.
-
1
def show
-
#FIXME not authorizing may be a bad idea. We are creating a public page here... ?
-
#@sprint.wiki_page does actually return wiki_page_title. Redmine titleizes this, so do we, even if it is redundant.
-
2
redirect_to :controller => 'wiki', :action => 'show', :project_id => @sprint.project, :id => Wiki.titleize(@sprint.wiki_page)
-
end
-
-
# NOTE: This method is public (see init.rb). We will let Redmine core's
-
# WikiController#index tak care of autorization
-
# NOTE: this redirection causes a page to be created from a template
-
# as a side-effect of calling @sprint.wiki_page
-
1
def edit
-
2
redirect_to :controller => 'wiki', :action => 'edit', :project_id => @sprint.project, :id => Wiki.titleize(@sprint.wiki_page)
-
end
-
end
-
1
require 'color'
-
1
require 'nokogiri'
-
-
1
module RbCommonHelper
-
1
unloadable
-
-
1
include CustomFieldsHelper
-
-
1
def assignee_id_or_empty(story)
-
438
story.new_record? ? "" : story.assigned_to_id
-
end
-
-
1
def assignee_name_or_empty(story)
-
444
story.blank? || story.assigned_to.blank? ? "" : "#{story.assigned_to.name}"
-
end
-
-
1
def blocked_ids(blocked)
-
136
blocked.map{|b| b.id }.join(',')
-
end
-
-
1
def build_inline_style(task)
-
339
if (task.blank? || task.assigned_to.blank? || !task.assigned_to.is_a?(User))
-
339
''
-
else
-
color_to = task.assigned_to.backlogs_preference[:task_color]
-
color_from = Backlogs::Color.new(color_to).lighten(0.5)
-
"style='
-
background-color:#{task.assigned_to.backlogs_preference[:task_color]};
-
background: -webkit-gradient(linear, left top, left bottom, from(#{color_from}), to(#{color_to}));
-
background: -moz-linear-gradient(top, #{color_from}, #{color_to});
-
filter:progid:DXImageTransform.Microsoft.Gradient(Enabled=1,GradientType=0,StartColorStr=#{color_from},EndColorStr=#{color_to});
-
'"
-
end
-
end
-
-
1
def breadcrumb_separator
-
131
"<span class='separator'>»</span>".html_safe
-
end
-
-
1
def description_or_empty(story)
-
story.new_record? ? "" : textilizable(story, :description)
-
end
-
-
1
def id_or_empty(item)
-
909
item.new_record? ? "" : item.id
-
end
-
-
1
def issue_link_or_empty(item)
-
663
item_id = item.id.to_s
-
663
text = (item_id.length > 8 ? "#{item_id[0..1]}...#{item_id[-4..-1]}" : item_id)
-
663
item.new_record? ? "" : link_to(text, {:controller => "issues", :action => "show", :id => item}, {:target => "_blank", :class => "prevent_edit"})
-
end
-
-
1
def sprint_link_or_empty(item)
-
242
item_id = item.id.to_s
-
242
text = (item_id.length > 8 ? "#{item_id[0..1]}...#{item_id[-4..-1]}" : item_id)
-
242
item.new_record? ? "" : link_to(text, {:controller => 'versions', :action => "show", :id => item}, {:target => "_blank", :class => "prevent_edit"})
-
end
-
-
1
def release_link_or_empty(release)
-
20
release.new_record? ? "" : link_to(release.name, {:controller => "rb_releases", :action => "show", :release_id => release})
-
end
-
-
1
def mark_if_closed(story)
-
768
!story.new_record? && story.status.is_closed? ? "closed" : ""
-
end
-
-
1
def story_points_or_empty(story)
-
987
story.story_points.blank? ? "" : story.story_points
-
end
-
-
1
def record_id_or_empty(story)
-
story.new_record? ? "" : story.id
-
end
-
-
1
def release_or_empty(story)
-
105
story.release_id.nil? ? "" : RbRelease.find(story.release_id).name
-
end
-
-
1
def sprint_status_id_or_default(sprint)
-
sprint.new_record? ? Version::VERSION_STATUSES.first : sprint.status
-
end
-
-
1
def sprint_status_label_or_default(sprint)
-
sprint.new_record? ? l("version_status_#{Version::VERSION_STATUSES.first}") : l("version_status_#{sprint.status}")
-
end
-
-
1
def status_id_or_default(story)
-
429
story.new_record? ? IssueStatus.default.id : story.status.id
-
end
-
-
1
def status_label_or_default(story)
-
534
story.new_record? ? IssueStatus.default.name : story.status.name
-
end
-
-
1
def sprint_html_id_or_empty(sprint)
-
242
sprint.new_record? ? "" : "sprint_#{sprint.id}"
-
end
-
-
1
def story_html_id_or_empty(story)
-
429
story.new_record? ? "" : "story_#{story.id}"
-
end
-
-
1
def release_html_id_or_empty(release)
-
20
release.new_record? ? "" : "release_#{release.id}"
-
end
-
-
1
def textile_description_or_empty(story)
-
story.new_record? ? "" : h(story.description).gsub(/<(\/?pre)>/, '<\1>')
-
end
-
-
1
def tracker_id_or_empty(story)
-
663
story.new_record? ? "" : story.tracker_id
-
end
-
-
1
def tracker_name_or_empty(story)
-
453
story.new_record? ? "" : story.tracker.name
-
end
-
-
1
def project_name_or_empty(story)
-
105
story.new_record? ? "" : story.project.name
-
end
-
-
1
def custom_fields_or_empty(story)
-
105
return '' if story.new_record?
-
105
res = ''
-
105
story.custom_field_values.each{|value|
-
res += "<p><b>#{h(value.custom_field.name)}</b>: #{simple_format_without_paragraph(h(show_value(value)))}</p>"
-
}
-
105
res.html_safe
-
end
-
-
1
def updated_on_with_milliseconds(story)
-
date_string_with_milliseconds(story.updated_on, 0.001) unless story.blank?
-
end
-
-
1
def date_string_with_milliseconds(d, add=0)
-
102
return '' if d.blank?
-
95
d.strftime("%B %d, %Y %H:%M:%S") + '.' + (d.to_f % 1 + add).to_s.split('.')[1] + d.strftime(" %z")
-
end
-
-
1
def remaining_hours_or_empty(item)
-
192
item.remaining_hours.blank? || item.remaining_hours==0 ? "" : item.remaining_hours
-
end
-
-
1
def workdays(start_day, end_day)
-
return (start_day .. end_day).select {|d| (d.wday > 0 and d.wday < 6) }
-
end
-
-
1
def release_burndown_interpolate(release, day)
-
initial_day = release.burndown.days[0]
-
initial_points = release.burndown.remaining_story_points[0]
-
day_diff = initial_points / (release.days.size - 1.0)
-
initial_points - ( (workdays(initial_day, day).size - 1) * day_diff )
-
end
-
-
1
def release_burndown_to_csv(release)
-
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
-
-
# FIXME decimal_separator is not used, instead a hardcoded s/\./,/g is done
-
# below to make (German) Excel happy
-
#decimal_separator = l(:general_csv_decimal_separator)
-
-
export = FCSV.generate(:col_sep => ';') do |csv|
-
# csv header fields
-
headers = [ l(:label_points_backlog),
-
l(:label_points_added),
-
l(:label_points_accepted)
-
]
-
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
-
-
bd = release.burndown
-
lines = 0
-
lines = bd[:added_points].size unless bd[:added_points].nil?
-
for i in (0..(lines-1))
-
fields = [ bd[:added_points][i].to_s.gsub('.', ','),
-
bd[:backlog_points][i].to_s.gsub('.', ','),
-
bd[:closed_points][i].to_s.gsub('.', ',')
-
]
-
csv << fields.collect{ |c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
-
end
-
end
-
export
-
end
-
-
1
def self.find_backlogs_enabled_active_projects
-
1
projects = EnabledModule.find(:all,
-
:conditions => ["enabled_modules.name = 'backlogs' and status = ?", Project::STATUS_ACTIVE],
-
:include => :project,
-
1
:joins => :project).collect { |mod| mod.project}
-
end
-
-
# Returns a collection of users allowed to log time for the current project. (see app/views/rb_taskboards/show.html.erb for usage)
-
1
def users_allowed_to_log_on_task
-
38
@project.memberships.collect{|m|
-
114
user = m.user
-
114
roles = user ? user.roles_for_project(@project) : nil
-
228
roles && roles.detect {|role| role.member? && role.allowed_to?(:log_time)} ? [user.name, user.id] : nil
-
}.compact.insert(0,["",0]) # Add blank entry
-
end
-
-
1
def tidy(html)
-
return Nokogiri::HTML::fragment(html).to_xhtml
-
end
-
-
1
def users_assignable_options_for_select(collection)
-
38
s = ''
-
38
groups = ''
-
-
38
if collection.include?(User.current)
-
38
el = User.current
-
38
s << "<option value=\"#{el.id}\" color=\"#{el.backlogs_preference[:task_color]}\" color_light=\"#{el.backlogs_preference[:task_color_light]}\"><< #{l(:label_me)} >></option>"
-
end
-
-
38
collection.sort.each do |element|
-
76
if element.is_a?(Group)
-
groups << "<option value=\"#{element.id}\" color=\"#AAAAAA\" color_light=\"#E0E0E0\">#{h element.name}</option>"
-
else
-
76
s << "<option value=\"#{element.id}\" color=\"#{element.backlogs_preference[:task_color]}\" color_light=\"#{element.backlogs_preference[:task_color_light]}\">#{h element.name}</option>"
-
end
-
end
-
38
unless groups.empty?
-
s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
-
end
-
38
s.html_safe
-
end
-
-
1
def release_options_for_select(releases, selected=nil)
-
grouped = Hash.new {|h,k| h[k] = []}
-
releases.each do |release|
-
grouped[release.project.name] << [release.name, release.id]
-
end
-
# Add in the selected
-
if selected && !releases.include?(selected)
-
grouped[selected.project.name] << [selected.name, selected.id]
-
end
-
-
if grouped.keys.size > 1
-
grouped_options_for_select(grouped, selected && selected.id)
-
else
-
options_for_select((grouped.values.first || []), selected && selected.id)
-
end
-
end
-
-
1
def format_release_sharing(v)
-
12
RbRelease::RELEASE_SHARINGS.include?(v) ? l("label_version_sharing_#{v}") : "none"
-
end
-
-
#fixup rails base uri which is not obeyed IF url_for is used in a redmine layout hook
-
1
def url_for_prefix_in_hooks
-
20
if Rails::VERSION::MAJOR < 3
-
'' #actionpack-2.3.14/lib/action_controller/url_rewriter.rb is injecting relative_url_root
-
else
-
20
Redmine::Utils.relative_url_root #actionpack-3* is not???
-
end
-
end
-
end
-
1
module RbFormHelper
-
1
unloadable
-
-
1
def rb_form_for(*args, &proc)
-
form_string = form_for(*args, &proc)
-
if Rails::VERSION::MAJOR < 3
-
form_string
-
else
-
concat(form_string)
-
end
-
end
-
-
# Streamline the difference between <%= %> and <% %>
-
1
def rb_labelled_fields_for(*args, &proc)
-
3
fields_string = labelled_fields_for(*args, &proc)
-
3
if Rails::VERSION::MAJOR < 3
-
fields_string
-
else
-
3
concat(fields_string)
-
end
-
end
-
-
# Streamline the difference between <%= %> and <% %>
-
1
def rb_labelled_form_for(*args, &proc)
-
3
form_string = labelled_form_for(*args, &proc)
-
3
if Rails::VERSION::MAJOR < 3
-
form_string
-
else
-
3
concat(form_string)
-
end
-
end
-
-
end
-
1
module RbMasterBacklogsHelper
-
1
unloadable
-
1
include Redmine::I18n
-
-
1
def backlog_html_class(backlog)
-
is_sprint?(backlog) ? "sprint backlog" : "product backlog"
-
end
-
-
1
def backlog_html_id(backlog)
-
is_sprint?(backlog) ? "sprint_#{backlog.id}" : "product_backlog"
-
end
-
-
1
def backlog_id_or_empty(backlog)
-
is_sprint?(backlog) ? backlog.id : ""
-
end
-
-
1
def backlog_menu(is_sprint, items = [])
-
html = %{
-
<div class="menu">
-
<div class="icon ui-icon ui-icon-carat-1-s"></div>
-
<ul class="items">
-
}
-
items.each do |item|
-
item[:condition] = true unless item.has_key?(:condition)
-
if item[:condition] && ( (is_sprint && item[:for] == :sprint) ||
-
(!is_sprint && item[:for] == :product) ||
-
(item[:for] == :both) )
-
html += %{ <li class="item">#{item[:item]}</li> }
-
end
-
end
-
html += %{
-
</ul>
-
</div>
-
}
-
end
-
-
1
def date_or_nil(date)
-
date.blank? ? '' : date.strftime('%Y-%m-%d')
-
end
-
-
1
def editable_if_sprint(backlog)
-
"editable" if is_sprint?(backlog)
-
end
-
-
1
def is_sprint?(backlog)
-
backlog.class.to_s.downcase=='sprint'
-
end
-
-
1
def menu_link(label, options = {})
-
# options[:class] = "pureCssMenui"
-
link_to(label, options)
-
end
-
-
1
def name_or_default(backlog)
-
is_sprint?(backlog) ? backlog.name : l(:label_Product_backlog)
-
end
-
-
1
def stories(backlog)
-
backlog[:stories] || backlog.stories
-
end
-
end
-
1
module RbServerVariablesHelper
-
1
unloadable
-
-
# Calculates workflow transitions matrix.
-
# Used to render server variables for javascript DnD handling
-
#
-
# workflow_transitions(RbStory)
-
1
def workflow_transitions(klass)
-
13
default_status = IssueStatus.default
-
13
default_status = default_status.id.to_s if default_status
-
13
roles = User.current.roles_for_project(@project)
-
13
transitions = {:states => {}, :transitions => {} , :default => default_status }
-
-
13
klass.trackers.each {|tracker_id|
-
13
tracker = Tracker.find(tracker_id)
-
13
tracker_id = tracker_id.to_s
-
-
13
transitions[:transitions][tracker_id] = {}
-
-
13
tracker.issue_statuses.each {|status|
-
78
status_id = status.id.to_s
-
-
78
transitions[:states][status_id] = {:name => status.name, :closed => (status.is_closed? ? l(:label_closed_issues) + ' ' : '')}
-
-
78
[[false, false], [true, true], [false, true], [true, false]].each{|creator, assignee|
-
312
key = "#{creator ? '+' : '-'}c#{assignee ? '+' : '-'}a"
-
-
312
transitions[:transitions][tracker_id][key] ||= {}
-
-
312
begin
-
312
allowed_statues = status.new_statuses_allowed_to(roles, tracker, creator, assignee)
-
rescue #Workaround in order to support redmine 1.1.3
-
allowed_statues = status.new_statuses_allowed_to(roles, tracker)
-
end
-
-
1872
allowed = allowed_statues.collect{|s| s.id.to_s}
-
-
312
transitions[:transitions][tracker_id][key][:default] ||= allowed[0]
-
-
312
allowed.unshift(status_id)
-
-
312
transitions[:transitions][tracker_id][key][status_id] = allowed.compact.uniq
-
}
-
}
-
}
-
13
transitions
-
end
-
end
-
1
require 'pp'
-
-
1
class RbIssueHistory < ActiveRecord::Base
-
1
set_table_name 'rb_issue_history'
-
1
belongs_to :issue
-
-
1
serialize :history, Array
-
1
after_save :touch_sprint
-
1
after_initialize :init_history
-
1
after_create :update_parent
-
-
1
def self.burndown_timezone(recalc=nil)
-
#provide a ActiveSupport::TimeZone to calculate burndown day boundaries. Configured in global settings.
-
#guarantees to return z timezone object, falling back gracefully.
-
#To be backward compatible, fallback to ENV['TZ'] (server_tz) is provided first - that was the old behavior
-
#Not considering ActiveSupport Time.zone (configured in config.time_zone for rails apps) - we provide our own configuration option
-
4280
@burndown_timezone = nil unless recalc.nil?
-
@burndown_timezone ||= begin
-
2
server_tz = ActiveSupport::TimeZone["Etc/GMT-#{Time.now.utc_offset/3600}"] rescue server_tz = nil
-
2
fallback_tz = server_tz || ActiveSupport::TimeZone["UTC"]
-
2
if Backlogs.settings[:burndown_timezone] #backlogs configuration for burndown day boundaries
-
ActiveSupport::TimeZone[Backlogs.settings[:burndown_timezone]] || fallback_tz
-
else
-
2
fallback_tz
-
end
-
4280
end
-
end
-
-
1
def self.statuses
-
5538
Hash.new{|h, k|
-
4100
s = IssueStatus.find_by_id(k.to_i)
-
4100
if s.nil?
-
s = IssueStatus.default
-
puts "IssueStatus #{k.inspect} not found, using default #{s.id} instead"
-
end
-
4100
h[k] = {:id => s.id, :open => ! s.is_closed?, :success => s.is_closed? ? (s.default_done_ratio.nil? || s.default_done_ratio == 100) : false }
-
4100
h[k]
-
}
-
end
-
-
1
def filter(sprint, status=nil)
-
149659
h = Hash[*(self.expand.collect{|d| [d[:date], d]}.flatten)]
-
41877
filtered = sprint.days.collect{|d| h[d] ? h[d] : {:date => d, :origin => :filter}}
-
-
# see if this issue was closed after sprint end
-
2384
if filtered[-1][:status_open]
-
184
self.history.select{|h| h[:date] > sprint.effective_date}.each{|h|
-
2
if h[:sprint] == sprint.id && !h[:status_open]
-
filtered[-1] = h
-
break
-
end
-
}
-
end
-
2384
return filtered
-
end
-
-
1
def filter_release(days)
-
531
h = Hash[*(self.expand.collect{|d| [d[:date], d]}.flatten)]
-
#if we have no day matching, find one earlier to get the latest status
-
17
filtered = days.collect{|d|
-
49
while !h[d] && d > days[0] #FIXME why did self.expand not give us all days in h?
-
if d > days[-1]
-
d = days[-1]
-
else
-
d = d.yesterday
-
end
-
end
-
49
h[d] ? h[d] : {:date => d, :origin => :filter}
-
}
-
-
# see if this issue was closed after last day
-
17
if filtered[-1][:status_open]
-
35
self.history.select{|h| h[:date] > days[-1]}.each{|h|
-
8
if !h[:status_open]
-
filtered[-1] = h
-
break
-
end
-
}
-
end
-
17
return filtered
-
end
-
-
1
def self.issue_type(tracker_id)
-
4100
return nil if tracker_id.nil? || tracker_id == ''
-
4100
tracker_id = tracker_id.to_i
-
4100
return :story if RbStory.trackers && RbStory.trackers.include?(tracker_id)
-
475
return :task if tracker_id == RbTask.tracker
-
return nil
-
end
-
-
1
def expand
-
# return a history array without gaps. If history has gaps, fill them with consecutive copies of each gap start day
-
2534
((0..self.history.size - 2).to_a.collect{|i|
-
3431
(self.history[i][:date] .. self.history[i+1][:date] - 1).to_a.collect{|d|
-
145521
self.history[i].merge(:date => d)
-
}
-
2534
} + [self.history[-1]]).flatten
-
end
-
-
1
def self.rebuild_issue(issue, status=nil)
-
rb = RbIssueHistory.find_or_initialize_by_issue_id(issue.id)
-
-
rb.history = [{:date => issue.created_on.to_date - 1, :origin => :rebuild}]
-
-
status ||= self.statuses
-
-
convert = lambda {|prop, v|
-
if v.to_s == ''
-
nil
-
elsif [:estimated_hours, :remaining_hours, :story_points].include?(prop)
-
Float(v)
-
else
-
Integer(v)
-
end
-
}
-
-
full_journal = {}
-
issue.journals.each{|journal|
-
date = journal.created_on.to_date
-
-
## TODO: SKIP estimated_hours and remaining_hours if not a leaf node
-
journal.details.each{|jd|
-
next unless jd.property == 'attr' && ['estimated_hours', 'story_points', 'remaining_hours', 'fixed_version_id', 'status_id', 'tracker_id'].include?(jd.prop_key)
-
-
prop = jd.prop_key.intern
-
update = {:old => convert.call(prop, jd.old_value), :new => convert.call(prop, jd.value)}
-
-
full_journal[date] ||= {}
-
-
case prop
-
when :estimated_hours, :remaining_hours # these sum to their parents
-
full_journal[date][prop] = update
-
when :story_points
-
full_journal[date][prop] = update
-
when :fixed_version_id
-
full_journal[date][:sprint] = update
-
when :status_id
-
[:id, :open, :success].each_with_index{|status_prop, i|
-
full_journal[date]["status_#{status_prop}".intern] = {:old => status[update[:old]][status_prop], :new => status[update[:new]][status_prop]}
-
}
-
when :tracker_id
-
full_journal[date][:tracker] = {:old => RbIssueHistory.issue_type(update[:old]), :new => RbIssueHistory.issue_type(update[:new])}
-
else
-
raise "Unhandled property #{jd.prop}"
-
end
-
}
-
}
-
-
if ActiveRecord::Base.connection.tables.include?('rb_journals')
-
RbJournal.all(:conditions => ['issue_id=?', issue.id], :order => 'timestamp asc').each{|j|
-
date = j.timestamp.to_date
-
full_journal[date] ||= {}
-
case j.property
-
when 'story_points' then full_journal[date][:story_points] = {:new => j.value ? j.value.to_f : nil}
-
when 'status_success' then full_journal[date][:status_success] = {:new => j.value == 'true'}
-
when 'status_open' then full_journal[date][:status_open] = {:new => j.value == 'true'}
-
when 'fixed_version_id' then full_journal[date][:sprint] = {:new => j.value ? j.value.to_i : nil}
-
when 'estimated_hours' then full_journal[date][:estimated_hours] = {:new => j.value ? j.value.to_f : nil}
-
when 'remaining_hours' then full_journal[date][:remaining_hours] = {:new => j.value ? j.value.to_f : nil}
-
-
else raise "Unexpected property #{j.property}: #{j.value.inspect}"
-
end
-
-
#:status_id is not in rb_journals
-
-
full_journal[date][:tracker] ||= {:new =>
-
case
-
when issue.is_story? then :story
-
when issue.is_task? then :task
-
else nil
-
end
-
}
-
}
-
end
-
-
full_journal[issue.updated_on.to_date] = {
-
:story_points => {:new => issue.story_points},
-
:sprint => {:new => issue.fixed_version_id },
-
:status_id => {:new => issue.status_id },
-
:status_open => {:new => status[issue.status_id][:open] },
-
:status_success => {:new => status[issue.status_id][:success] },
-
:tracker => {:new => RbIssueHistory.issue_type(issue.tracker_id) },
-
:estimated_hours => {:new => issue.estimated_hours},
-
:remaining_hours => {:new => issue.remaining_hours},
-
}
-
-
# Wouldn't be needed if redmine just created journals for update_parent_properties
-
subissues = Issue.find(:all, :conditions => ['parent_id = ?', issue.id]).to_a
-
subhists = []
-
subdates = []
-
subissues.each{|i|
-
subdates.concat(i.history.history.collect{|h| h[:date]})
-
subhists << Hash[*(i.history.expand.collect{|d| [d[:date], d]}.flatten)]
-
}
-
subdates.uniq!
-
subdates.sort!
-
-
subdates.sort.each{|date|
-
next if date < issue.created_on.to_date
-
-
current = {}
-
full_journal.keys.sort.select{|d| d <= date}.each{|d|
-
current[:sprint] = full_journal[d][:sprint][:new] if full_journal[d][:sprint]
-
current[:estimated_hours] = full_journal[d][:estimated_hours][:new] if full_journal[d][:estimated_hours]
-
current[:remaining_hours] = full_journal[d][:remaining_hours][:new] if full_journal[d][:remaining_hours]
-
current[:tracker] = full_journal[d][:tracker][:new] if full_journal[d][:tracker]
-
}
-
next unless current[:tracker] # only process issues that exist at that date and are either story or task
-
-
change = {
-
:sprint => [],
-
:estimated_hours => [],
-
:remaining_hours => [],
-
}
-
subhists.each{|h|
-
[:sprint, :remaining_hours, :estimated_hours].each{|prop|
-
change[prop] << h[date][prop] if h[date] && h[date].include?(prop)
-
}
-
}
-
change[:sprint].uniq!
-
change[:sprint].sort!{|a, b|
-
if a.nil? && b.nil?
-
0
-
elsif a.nil?
-
1
-
elsif b.nil?
-
-1
-
else
-
a <=> b
-
end
-
}
-
-
[:remaining_hours, :estimated_hours].each{|prop|
-
if change[prop].size == 0
-
change.delete(prop)
-
else
-
change[prop] = change[prop].compact.sum
-
end
-
}
-
-
if change[:sprint].size != 0 && current[:sprint] != change[:sprint][0]
-
full_journal[date] ||= {}
-
full_journal[date][:sprint] = {:old => current[:sprint], :new => change[:sprint][0]}
-
end
-
if change.include?(:estimated_hours) && current[:estimated_hours] != change[:estimated_hours]
-
full_journal[date] ||= {}
-
full_journal[date][:estimated_hours] = {:old => current[:estimated_hours], :new => change[:estimated_hours]}
-
end
-
if change.include?(:remaining_hours) && current[:remaining_hours] != change[:remaining_hours]
-
full_journal[date] ||= {}
-
full_journal[date][:remaining_hours] = {:old => current[:remaining_hours], :new => change[:remaining_hours]}
-
end
-
}
-
# End of child journal picking
-
-
# process combined journal in order of timestamp into final history
-
full_journal.keys.sort.collect{|date| {:date => date, :update => full_journal[date]} }.each {|entry|
-
if entry[:date] != rb.history[-1][:date]
-
rb.history << rb.history[-1].dup
-
rb.history[-1][:date] = entry[:date]
-
end
-
-
entry[:update].each_pair{|prop, old_new|
-
rb.history[0][prop] = old_new[:old] if old_new.include?(:old) && !rb.history[0].include?(prop)
-
rb.history[-1][prop] = old_new[:new]
-
rb.history.each{|h| h[prop] = old_new[:new] unless h.include?(prop) }
-
}
-
}
-
-
# fill out journal so each journal entry is complete on each day
-
rb.history.each{|h|
-
h[:estimated_hours] = issue.estimated_hours unless h.include?(:estimated_hours)
-
h[:story_points] = issue.story_points unless h.include?(:story_points)
-
h[:remaining_hours] = issue.remaining_hours unless h.include?(:remaining_hours)
-
h[:tracker] = RbIssueHistory.issue_type(issue.tracker_id) unless h.include?(:tracker)
-
h[:sprint] = issue.fixed_version_id unless h.include?(:sprint)
-
h[:status_open] = status[issue.status_id][:open] unless h.include?(:status_open)
-
h[:status_success] = status[issue.status_id][:success] unless h.include?(:status_success)
-
-
h[:hours] = h[:remaining_hours] || h[:estimated_hours]
-
}
-
rb.history[-1][:hours] = rb.history[-1][:remaining_hours] || rb.history[-1][:estimated_hours]
-
rb.history[0][:hours] = rb.history[0][:estimated_hours] || rb.history[0][:remaining_hours]
-
-
rb.save
-
-
if rb.history.detect{|h| h[:tracker] == :story }
-
rb.history.collect{|h| h[:sprint] }.compact.uniq.each{|sprint_id|
-
sprint = RbSprint.find_by_id(sprint_id)
-
next unless sprint
-
sprint.burndown.touch!(issue.id)
-
}
-
end
-
end
-
-
1
def self.rebuild
-
RbSprintBurndown.delete_all
-
-
status = self.statuses
-
-
issues = Issue.count
-
Issue.find(:all, :order => 'root_id asc, lft desc').each_with_index{|issue, n|
-
puts "#{issue.id.to_s.rjust(6, ' ')} (#{(n+1).to_s.rjust(6, ' ')}/#{issues})..."
-
RbIssueHistory.rebuild_issue(issue, status)
-
}
-
end
-
-
1
def init_history
-
4100
self.history ||= []
-
4100
_issue = self.issue
-
-
4100
_statuses ||= self.class.statuses
-
4100
current = {
-
:estimated_hours => _issue.estimated_hours,
-
:story_points => _issue.story_points,
-
:remaining_hours => _issue.remaining_hours,
-
:tracker => RbIssueHistory.issue_type(_issue.tracker_id),
-
:sprint => _issue.fixed_version_id,
-
:release => _issue.release_id,
-
:status_id => _issue.status_id,
-
:status_open => _statuses[_issue.status_id][:open],
-
:status_success => _statuses[_issue.status_id][:success],
-
:origin => :default
-
}
-
-
#a sprint day lasts from 00:00:00 to 23:59:59 in configured timezone
-
#Get the burndown_timezone || server-tz || utc as ActiveSupport::TimeZone object
-
4100
todo = []
-
4100
_today = self.class.burndown_timezone.now.to_date # current date in terms of burndown day boundary
-
4100
todo << _today.yesterday if self.history.size == 0
-
4100
todo << _today if self.history.size == 0 || self.history[-1][:date] != _today
-
4100
if todo.size > 0
-
1275
todo.each{|date|
-
2137
self.history << {:date => date}.merge(current)
-
2137
self.history[-1][:hours] = self.history[-1][:remaining_hours] || self.history[-1][:estimated_hours]
-
}
-
end
-
-
4100
self.history[-1].merge!(current)
-
4100
self.history[-1][:hours] = self.history[-1][:remaining_hours] || self.history[-1][:estimated_hours]
-
4100
self.history[0][:hours] = self.history[0][:estimated_hours] || self.history[0][:remaining_hours]
-
-
4100
raise "init_history failed: #{todo.inspect} => #{self.history.inspect}" unless self.history.size >= 2
-
end
-
-
1
def touch_sprint
-
12405
self.history.select{|h| h[:sprint]}.uniq{|h| "#{h[:sprint]}::#{h[:tracker]}"}.each{|h|
-
1911
sprint = RbSprint.find_by_id(h[:sprint])
-
1911
next unless sprint
-
1911
sprint.burndown.touch!(h[:tracker] == :story ? self.issue.id : nil)
-
}
-
end
-
-
# normally, the update_parent_attributes of redmine would take care of re-saving parent issues where necessasy and thereby causing a history
-
# regeneration. The exception to this is the creation-record in the history. This function handles that exception.
-
# Upon each save, the history record for date `today' is set to the current value. That way, the history record for any date always holds the
-
# latest value for that day -- essentially, the value it still had at midnight that day. So for the creation date of an issue, the history entry
-
# for the date is was created holds the last value it had on that date. The value that the issue had (for the relevant properties that is) is
-
# stored in the first history entry, dated the day *before* the creation date; it holds the values it had `midnight the day before', which
-
# for our purposes means `the value it had at the start of the creation day'. This is very convenient for burndown calculation purposes, but
-
# during the cascading re-save of parent issues, no save is triggered for the `yesterday' update, so properties that are calculated as the sum
-
# of the children of an issue would be forgotton. To remedy this, this function is called after save of the history and updates the parent
-
# history recursively *for that day only*. It will only be called once, which is when the history is created.
-
1
def update_parent(date=nil)
-
980
if (p = self.issue.parent) # if no parent, nothing to do
-
118
date ||= self.history[0][:date] # the after_create calls this function without a parameter, so we know it's the creation call. Get the `yesterday' entry.
-
236
parent_history_index = p.history.history.index{|d| d[:date] == date} # does the parent have an history entry on that date?
-
118
if parent_history_index.nil? # if not, stretch the history to get the values at that date
-
parent_data = p.history.expand.detect{|d| d[:date] == date}
-
else # if so, grab that entry
-
118
parent_data = p.history.history[parent_history_index]
-
end
-
-
# if no entry is found, that means no history entry exists between that creation date and now, so the parent was created after the task. Nothing to do.
-
118
return unless parent_data
-
-
# we know this parent has children, because a child triggered this. Set the calculated fields to nil.
-
472
[:estimated_hours, :remaining_hours, :hours].each{|h| parent_data[h] = nil }
-
118
p.children.each{|child|
-
266
child_data = child.history.expand.detect{|d| d[:date] == date } # get the history record for the child for that date
-
133
next unless child_data # child didn't exist then, next
-
-
# sum these values, if the child has any value for them. This keeps the value nil if all the children have it at nil.
-
532
[:estimated_hours, :remaining_hours, :hours].each{|h| parent_data[h] = parent_data[h].to_i + child_data[h] if child_data[h] }
-
}
-
-
118
if parent_history_index.nil?
-
# the record needs to be added, so add and sort (history needs to be sorted)
-
p.history.history = (p.history.history + [parent_data]).sort{|a, b| a[:date] <=> b[:date]}
-
else
-
# there was an entry on this date, replace it
-
118
p.history.history[parent_history_index] = parent_data
-
end
-
118
p.history.save
-
-
# cascade, but pass on the date initialized by the after_create invocation.
-
118
p.history.update_parent(date)
-
end
-
end
-
end
-
1
class RbProjectSettings < ActiveRecord::Base
-
1
unloadable
-
1
belongs_to :project
-
end
-
-
1
require 'date'
-
-
1
class ReleaseBurndown
-
1
def initialize(release)
-
# @days = release.days
-
4
@release_id = release.id
-
4
@project = release.project
-
-
#initialize empty release
-
4
@data = {}
-
4
@data[:added_points] = []
-
4
@data[:added_points_pos] = []
-
4
@data[:backlog_points] = []
-
4
@data[:closed_points] = []
-
4
@data[:trend_added] = []
-
4
@data[:trend_closed] = []
-
-
# Select sprints within release period. They need not to be closed.
-
4
sprints = release.sprints
-
4
return if sprints.nil? || sprints.size == 0
-
-
4
baseline = [0] * (sprints.size + 1)
-
-
4
series = Backlogs::MergedArray.new
-
4
series.merge(:backlog_points => baseline.dup)
-
4
series.merge(:added_points => baseline.dup)
-
4
series.merge(:closed_points => baseline.dup)
-
-
#TODO Caching
-
#TODO Maybe utilize/extend sprint burndown data?
-
#TODO Stories continued over several sprints (by duplicating) should not show up as added
-
#TODO Likewise stories split from inital epics should not show up as added
-
-
# Go through each story in the backlog
-
4
release.stories.each{ |story|
-
16
series.add(story.release_burndown_data(sprints))
-
}
-
-
# Series collected, now format data for jqplot
-
# Slightly hacky formatting to get the correct view. Might change when this jqplot issue is
-
# sorted out:
-
# See https://bitbucket.org/cleonello/jqplot/issue/181/nagative-values-in-stacked-bar-chart
-
#TODO Maybe move jqplot format stuff to releaseburndown view?
-
16
@data[:added_points] = series.collect{ |s| -1 * s.added_points }
-
16
@data[:added_points_pos] = series.collect{ |s| s.backlog_points >= 0 ? s.added_points : s.added_points + s.backlog_points }
-
16
@data[:backlog_points] = series.collect{ |s| s.backlog_points >= 0 ? s.backlog_points : 0 }
-
4
@data[:closed_points] = series.series(:closed_points)
-
-
-
# Forecast (probably just as good as the weather forecast...)
-
#TODO Move forecast to RbRelease?
-
4
@data[:trend_closed] = Array.new
-
4
@data[:trend_added] = Array.new
-
4
avg_count = 3
-
4
if release.closed_sprints.size >= avg_count
-
avg_added = (@data[:added_points][-1] - @data[:added_points][-avg_count]) / avg_count
-
avg_closed = @data[:closed_points][-avg_count..-1].inject(0){|sum,p| sum += p} / avg_count
-
current_backlog = @data[:added_points][-1] + @data[:added_points_pos][-1] + @data[:backlog_points][-1]
-
current_added = @data[:added_points][-1]
-
current_sprints = @data[:closed_points].size
-
-
# Add beginning and end dataset [sprint,points] for trendlines
-
@data[:trend_closed] << [current_sprints, current_backlog]
-
@data[:trend_closed] << [current_sprints + 10, current_backlog - avg_closed * 10]
-
@data[:trend_added] << [current_sprints, current_added]
-
@data[:trend_added] << [current_sprints + 10, current_added + avg_added * 10]
-
-
end
-
-
#TODO Estimate sprints left
-
4
sprints_left = [0] * 10
-
-
# Extend other series with empty datapoints up to the estimated number of sprints
-
# to format plot correctly
-
4
@data[:added_points].concat sprints_left.dup
-
4
@data[:added_points_pos].concat sprints_left.dup
-
4
@data[:backlog_points].concat sprints_left.dup
-
4
@data[:closed_points].concat sprints_left.dup
-
end
-
-
1
def [](i)
-
30
i = i.intern if i.is_a?(String)
-
30
return nil unless @data[i] # be graceful
-
#raise "No burn#{@direction} data series '#{i}', available: #{@data.keys.inspect}" unless @data[i]
-
30
return @data[i]
-
end
-
-
1
def series(select = :active)
-
return @data.keys.collect{ |k| k.to_s }
-
# return @available_series.values.select{|s| (select == :all) }.sort{|x,y| "#{x.name}" <=> "#{y.name}"}
-
end
-
-
1
attr_reader :days
-
1
attr_reader :release_id
-
1
attr_reader :max
-
-
1
attr_reader :remaining_story_points
-
1
attr_reader :ideal
-
end
-
-
1
class RbRelease < ActiveRecord::Base
-
1
self.table_name = 'releases'
-
-
1
RELEASE_STATUSES = %w(open closed)
-
1
RELEASE_SHARINGS = %w(none descendants hierarchy tree system)
-
-
1
unloadable
-
-
1
belongs_to :project, :inverse_of => :releases
-
1
has_many :issues, :class_name => 'RbStory', :foreign_key => 'release_id', :dependent => :nullify
-
-
1
validates_presence_of :project_id, :name, :release_start_date, :release_end_date
-
1
validates_inclusion_of :status, :in => RELEASE_STATUSES
-
1
validates_inclusion_of :sharing, :in => RELEASE_SHARINGS
-
1
validates_length_of :name, :maximum => 64
-
1
validate :dates_valid?
-
-
1
scope :open, :conditions => {:status => 'open'}
-
1
scope :closed, :conditions => {:status => 'closed'}
-
1
scope :visible, lambda {|*args| { :include => :project,
-
116
:conditions => Project.allowed_to_condition(args.first || User.current, :view_releases) } }
-
-
-
1
include Backlogs::ActiveRecord::Attributes
-
-
1
def to_s; name end
-
-
1
def closed?
-
1
status == 'closed'
-
end
-
-
1
def dates_valid?
-
19
errors.add(:base, l(:error_release_end_after_start)) if self.release_start_date >= self.release_end_date if self.release_start_date and self.release_end_date
-
end
-
-
1
def stories #compat
-
13
issues
-
end
-
-
#Return sprints that contain issues within this release
-
1
def sprints
-
15
RbSprint.where('id in (select distinct(fixed_version_id) from issues where release_id=?)', id)
-
end
-
-
# Return sprints closed within this release
-
1
def closed_sprints
-
8
sprints.where("versions.status = ?", "closed")
-
end
-
-
1
def stories_by_sprint
-
12
order = Backlogs.setting[:sprint_sort_order] == 'desc' ? 'DESC' : 'ASC'
-
#return issues sorted into sprints. Obviously does not return issues which are not in a sprint
-
#unfortunately, group_by returns unsorted results.
-
12
issues.joins(:fixed_version).includes(:fixed_version).order("versions.effective_date #{order}").group_by(&:fixed_version_id)
-
end
-
-
1
def days(cutoff = nil)
-
# assumes mon-fri are working days, sat-sun are not. this
-
# assumption is not globally right, we need to make this configurable.
-
cutoff = self.release_end_date if cutoff.nil?
-
workdays(self.release_start_date, cutoff)
-
end
-
-
1
def has_burndown?
-
#merge: is it neccessary to have closed sprints for burndown? I'd like to see it immediately
-
4
return !!(self.release_start_date and self.release_end_date && !self.closed_sprints.nil?)
-
end
-
-
1
def burndown
-
4
return nil if not self.has_burndown?
-
4
@cached_burndown ||= ReleaseBurndown.new(self)
-
4
return @cached_burndown
-
end
-
-
1
def today
-
ReleaseBurndownDay.find(:first, :conditions => { :release_id => self, :day => Date.today })
-
end
-
-
1
def remaining_story_points #FIXME merge bohansen_release_chart removed this
-
9
res = 0
-
45
stories.open.each {|s| res += s.story_points if s.story_points}
-
9
res
-
end
-
-
1
def allowed_sharings(user = User.current)
-
3
RELEASE_SHARINGS.select do |s|
-
15
if sharing == s
-
3
true
-
else
-
12
case s
-
when 'system'
-
# Only admin users can set a systemwide sharing
-
3
user.admin?
-
when 'hierarchy', 'tree'
-
# Only users allowed to manage versions of the root project can
-
# set sharing to hierarchy or tree
-
6
project.nil? || user.allowed_to?(:manage_versions, project.root)
-
else
-
3
true
-
end
-
end
-
end
-
end
-
-
1
def shared_to_projects(scope_project)
-
projects = []
-
Project.visible.find(:all, :order => 'lft').each{|_project| #exhaustive search FIXME (pa sharing)
-
projects << _project unless (_project.shared_releases.collect{|v| v.id} & [id]).empty?
-
}
-
projects
-
end
-
-
#migrate old date-based releases to relation-based
-
1
def self.integrate_implicit_stories
-
unless RbStory.trackers
-
puts "Redmine not configured, skipping release migratinos"
-
return
-
end
-
#each release from newest to oldest
-
RbRelease.order('release_end_date desc').each do |release|
-
if release.project.nil?
-
# Release comes from deleted project before dependency was added.
-
release.delete
-
else
-
release.project.versions.select{ |v| v.due_date && (v.due_date>=release.release_start_date && v.due_date<=release.release_end_date)
-
}.each do |version|
-
#each sprint that lies within the release
-
version.fixed_issues.where('tracker_id in (?)', RbStory.trackers).each { |issue|
-
#each issue in that version which is a story and does not belong to a release, yet
-
if issue.release_id.nil?
-
issue.release = release;
-
issue.save!
-
end
-
}
-
end #sprints
-
end #releases
-
end #if project.nil?
-
end
-
-
end
-
1
require 'date'
-
-
1
class RbSprint < Version
-
1
unloadable
-
-
1
validate :start_and_end_dates
-
-
1
def start_and_end_dates
-
393
errors.add(:base, "sprint_end_before_start") if self.effective_date && self.sprint_start_date && self.sprint_start_date >= self.effective_date
-
end
-
-
1
def self.rb_scope(symbol, func)
-
2
if Rails::VERSION::MAJOR < 3
-
named_scope symbol, func
-
else
-
2
scope symbol, func
-
end
-
end
-
-
1
rb_scope :open_sprints, lambda { |project|
-
71
order = Backlogs.setting[:sprint_sort_order] == 'desc' ? 'DESC' : 'ASC'
-
{
-
:order => "CASE sprint_start_date WHEN NULL THEN 1 ELSE 0 END #{order},
-
sprint_start_date #{order},
-
CASE effective_date WHEN NULL THEN 1 ELSE 0 END #{order},
-
effective_date #{order}",
-
:conditions => [ "status = 'open' and project_id = ?", project.id ] #FIXME locked, too?
-
71
}
-
}
-
-
#TIB ajout du scope :closed_sprints
-
1
rb_scope :closed_sprints, lambda { |project|
-
order = Backlogs.setting[:sprint_sort_order] == 'desc' ? 'DESC' : 'ASC'
-
{
-
:order => "CASE sprint_start_date WHEN NULL THEN 1 ELSE 0 END #{order},
-
sprint_start_date #{order},
-
CASE effective_date WHEN NULL THEN 1 ELSE 0 END #{order},
-
effective_date #{order}",
-
:conditions => [ "status = 'closed' and project_id = ?", project.id ]
-
}
-
}
-
-
#depending on sharing mode
-
#return array of projects where this sprint is visible
-
1
def shared_to_projects(scope_project)
-
15
projects = []
-
15
Project.visible.find(:all, :order => 'lft').each{|_project| #exhaustive search FIXME (pa sharing)
-
726
projects << _project unless (_project.shared_versions.collect{|v| v.id} & [id]).empty?
-
}
-
15
projects
-
end
-
-
1
def stories
-
224
return RbStory.sprint_backlog(self)
-
end
-
-
1
def points
-
return stories.inject(0){|sum, story| sum + story.story_points.to_i}
-
end
-
-
1
def has_wiki_page
-
1
return false if wiki_page_title.blank?
-
-
page = project.wiki.find_page(self.wiki_page_title)
-
return false if !page
-
-
template = find_wiki_template
-
return false if template && page.text == template.text
-
-
return true
-
end
-
-
1
def find_wiki_template
-
3
projects = [self.project] + self.project.ancestors
-
-
3
template = Backlogs.setting[:wiki_template]
-
3
if template =~ /:/
-
p, template = *template.split(':', 2)
-
projects << Project.find(p)
-
end
-
-
3
projects.compact!
-
-
3
projects.each{|p|
-
3
next unless p.wiki
-
3
t = p.wiki.find_page(template)
-
3
return t if t
-
}
-
return nil
-
end
-
-
1
def wiki_page
-
4
if ! project.wiki
-
return ''
-
end
-
-
4
self.update_attribute(:wiki_page_title, Wiki.titleize(self.name)) if wiki_page_title.blank?
-
-
4
page = project.wiki.find_page(self.wiki_page_title)
-
4
if !page
-
3
template = find_wiki_template
-
3
if template
-
3
page = WikiPage.new(:wiki => project.wiki, :title => self.wiki_page_title)
-
3
page.content = WikiContent.new
-
3
page.content.text = "h1. #{self.name}\n\n#{template.text}"
-
3
page.save!
-
end
-
end
-
-
4
return wiki_page_title
-
end
-
-
1
def eta
-
return nil if ! self.sprint_start_date
-
-
dpp = self.project.scrum_statistics.info[:average_days_per_point]
-
return nil if !dpp
-
-
derived_days = if Backlogs.setting[:include_sat_and_sun]
-
Integer(self.points * dpp)
-
else
-
# 5 out of 7 are working days
-
Integer(self.points * dpp * 7.0/5)
-
end
-
return self.sprint_start_date + derived_days
-
end
-
-
1
def activity
-
bd = self.burndown
-
-
# assume a sprint is active if it's only 2 days old
-
return true if bd[:hours_remaining] && bd[:hours_remaining].compact.size <= 2
-
-
return Issue.exists?(['fixed_version_id = ? and ((updated_on between ? and ?) or (created_on between ? and ?))', self.id, -2.days.from_now, Time.now, -2.days.from_now, Time.now])
-
end
-
-
1
def impediments
-
@impediments ||= Issue.find(:all,
-
:conditions => ["id in (
-
select issue_from_id
-
from issue_relations ir
-
join issues blocked
-
on blocked.id = ir.issue_to_id
-
and blocked.tracker_id in (?)
-
and blocked.fixed_version_id = (?)
-
where ir.relation_type = 'blocks'
-
)",
-
RbStory.trackers + [RbTask.tracker],
-
self.id]
-
232
) #.sort {|a,b| a.closed? == b.closed? ? a.updated_on <=> b.updated_on : (a.closed? ? 1 : -1) }
-
end
-
end
-
1
require 'date'
-
1
require 'yaml'
-
-
1
class RbSprintBurndown < ActiveRecord::Base
-
1
set_table_name 'rb_sprint_burndown'
-
1
belongs_to :version
-
-
1
serialize :stories, Array
-
1
serialize :burndown, Hash
-
1
after_initialize :init
-
-
1
def direction
-
@direction
-
end
-
1
def direction=(dir)
-
2352
dir = :up if dir.to_s == ''
-
2352
dir = dir.intern if dir.is_a?(String)
-
2352
raise "Direction can only be 'up' or 'down', not #{dir.inspect}" unless [:up, :down].include?(dir)
-
2352
@direction = dir
-
end
-
-
1
def touch!(story_id = nil)
-
2313
if story_id
-
1254
story_id = Integer(story_id)
-
1254
return if self.stories.include?(story_id)
-
363
self.stories << story_id
-
end
-
1422
self.burndown = nil
-
1422
self.save!
-
end
-
-
# This causes a recursive call to recalculate. I don't know why yet
-
# def [](key)
-
# self.recalculate!
-
# key = key.intern if key.is_a?(String)
-
# raise "No burn#{@direction} data series '#{key}', available: #{self.burndown[@direction].keys.inspect}" unless self.burndown[@direction][key]
-
# return self.burndown[@direction][key]
-
# end
-
-
1
def series(remove_empty = true)
-
15
@series ||= {}
-
15
key = "#{@direction}_#{remove_empty ? 'filled' : 'all'}"
-
15
if @series[key].nil?
-
88
@series[key] = self.burndown[@direction].keys.collect{|k| k.to_s}.sort
-
11
if remove_empty
-
# delete :points_committed if flatline
-
11
@series[key].delete('points_committed') if self.burndown[@direction][:points_committed].uniq.compact.size < 1
-
-
# delete any series that is flat-line 0/nil
-
11
@series[key].each {|k|
-
957
@series[key].delete(k) if k != 'points_committed' && self.burndown[@direction][k.intern].collect{|d| d.to_f }.uniq == [0.0]
-
}
-
end
-
end
-
-
15
return @series[key]
-
end
-
-
#compatibility
-
1
def days
-
return self.burndown[:days]
-
end
-
-
1
def data
-
72
return self.burndown[@direction]
-
end
-
-
1
def init
-
2329
self.stories ||= []
-
2329
self.direction = Backlogs.setting[:points_burn_direction]
-
end
-
-
1
def burndown
-
2043
return @_burndown if defined?(@_burndown)
-
-
1438
@_burndown = read_attribute(:burndown)
-
1438
@_burndown = nil if !@_burndown || @_burndown.size == 0
-
-
# if I use self.version.id I get a "stack level too deep?!
-
1438
sprint = self.version # RbSprint.find(self.version_id)
-
-
1438
if !sprint.has_burndown?
-
@_burndown = nil
-
else
-
1438
@_burndown = {}
-
1438
days = sprint.days
-
1438
ndays = days.size
-
7190
[:hours_remaining, :points_committed, :points_accepted, :points_resolved].each{|k| @_burndown[k] = [nil] * ndays }
-
1438
statuses = RbIssueHistory.statuses
-
-
1438
RbStory.find(:all, :conditions => ['id in (?)', self.stories]).each{|story|
-
2384
bd = story.burndown(sprint, statuses)
-
2384
next unless bd
-
2384
bd.each_pair {|k, data|
-
9536
data.each_with_index{|d, i|
-
157972
next unless d
-
6379
@_burndown[k][i] ||= 0
-
6379
@_burndown[k][i] += d.to_f
-
}
-
}
-
}
-
-
1438
@_burndown[:ideal] = (0..ndays - 1).to_a.reverse
-
1438
[[:points_to_resolve, :points_resolved], [:points_to_accept, :points_accepted]].each{|todo|
-
2876
tgt, src = *todo
-
2876
@_burndown[tgt] = (0..ndays - 1).to_a.collect{|i|
-
52118
@_burndown[:points_committed][i] && @_burndown[src][i] ? @_burndown[:points_committed][i] - @_burndown[src][i] : nil
-
}
-
}
-
-
1438
[[:points_required_burn_rate, :points_to_resolve], [:hours_required_burn_rate, :hours_remaining]].each{|todo|
-
2876
tgt, src = *todo
-
2876
@_burndown[tgt] = (0..ndays - 1).to_a.collect{|i|
-
52118
@_burndown[src][i] ? Float(@_burndown[src][i]) / (@_burndown[:ideal][i] == 0 ? 1 : @_burndown[:ideal][i]) : nil
-
}
-
}
-
-
14380
@_burndown = { :up => @_burndown.reject{|k, v| [:points_to_resolve, :points_to_accept].include?(k) },
-
12942
:down => @_burndown.reject{|k, v| [:points_resolved, :points_accepted].include?(k) },
-
:days => days
-
}
-
end
-
-
1438
cur = read_attribute(:burndown)
-
1438
write_attribute(:burndown, @_burndown)
-
1438
self.save if @_burndown != cur
-
1438
return @_burndown
-
end
-
end
-
1
class RbStory < Issue
-
1
unloadable
-
-
1
private
-
-
1
def self.__find_options_normalize_option(option)
-
1202
option = [option] if option && !option.is_a?(Array)
-
1675
option = option.collect{|s| s.is_a?(Integer) ? s : s.id} if option
-
end
-
-
1
def self.__find_options_add_permissions(options)
-
601
permission = options.delete(:permission)
-
601
permission = false if permission.nil?
-
-
601
options[:conditions] ||= []
-
601
if permission
-
if Issue.respond_to? :visible_condition
-
visible = Issue.visible_condition(User.current, :project => project || Project.find(project_id))
-
else
-
visible = Project.allowed_to_condition(User.current, :view_issues)
-
end
-
Backlogs::ActiveRecord.add_condition(options, visible)
-
end
-
end
-
-
1
def self.__find_options_sprint_condition(project_id, sprint_ids)
-
457
if Backlogs.settings[:sharing_enabled]
-
["
-
tracker_id in (?)
-
179
and fixed_version_id IN (?)", self.trackers, sprint_ids]
-
else
-
["
-
project_id = ?
-
and tracker_id in (?)
-
278
and fixed_version_id IN (?)", project_id, self.trackers, sprint_ids]
-
end
-
end
-
-
1
def self.__find_options_release_condition(project_id, release_ids)
-
["
-
18
project_id in (#{Project.find(project_id).projects_in_shared_product_backlog.map{|p| p.id}.join(',')})
-
and tracker_id in (?)
-
and fixed_version_id is NULL
-
16
and release_id in (?)", self.trackers, release_ids]
-
end
-
-
1
def self.__find_options_pbl_condition(project_id)
-
["
-
236
project_id in (#{Project.find(project_id).projects_in_shared_product_backlog.map{|p| p.id}.join(',')})
-
and tracker_id in (?)
-
and release_id is NULL
-
and fixed_version_id is NULL
-
128
and is_closed = ?", self.trackers, false]
-
end
-
-
1
public
-
-
1
def self.find_options(options)
-
601
options = options.dup
-
-
601
project = options.delete(:project)
-
601
if project.nil?
-
project_id = nil
-
elsif project.is_a?(Integer)
-
551
project_id = project
-
551
project = nil
-
else
-
50
project_id = project.id
-
end
-
-
601
self.__find_options_add_permissions(options)
-
-
601
sprint_ids = self.__find_options_normalize_option(options.delete(:sprint))
-
601
release_ids = self.__find_options_normalize_option(options.delete(:release))
-
-
601
if sprint_ids
-
457
Backlogs::ActiveRecord.add_condition(options, self.__find_options_sprint_condition(project_id, sprint_ids))
-
elsif release_ids
-
16
Backlogs::ActiveRecord.add_condition(options, self.__find_options_release_condition(project_id, release_ids))
-
else #product backlog
-
128
Backlogs::ActiveRecord.add_condition(options, self.__find_options_pbl_condition(project_id))
-
128
options[:joins] ||= []
-
128
options[:joins] [options[:joins]] unless options[:joins].is_a?(Array)
-
128
options[:joins] << :status
-
128
options[:joins] << :project
-
end
-
-
601
options
-
end
-
-
493
scope :backlog_scope, lambda{|opts| RbStory.find_options(opts) }
-
-
1
def self.inject_lower_higher
-
prev = nil
-
i = 1
-
all.map {|story|
-
#optimization: set virtual attributes to avoid hundreds of sql queries
-
# this requires that the scope is clean - meaning exactly ONE backlog is queried here.
-
prev.higher_item = story if prev
-
story.lower_item = prev
-
prev = story
-
}
-
end
-
-
1
def self.backlog(project_id, sprint_id, release_id, options={})
-
474
self.visible.order("#{self.table_name}.position").
-
backlog_scope(
-
options.merge({
-
:project => project_id,
-
:sprint => sprint_id,
-
:release => release_id
-
}))
-
end
-
-
1
def self.product_backlog(project, limit=nil)
-
58
return RbStory.backlog(project.id, nil, nil, :limit => limit)
-
end
-
-
1
def self.sprint_backlog(sprint, options={})
-
224
return RbStory.backlog(sprint.project.id, sprint.id, nil, options)
-
end
-
-
1
def self.release_backlog(release, options={})
-
6
return RbStory.backlog(release.project.id, nil, release.id, options)
-
end
-
-
1
def self.backlogs_by_sprint(project, sprints, options={})
-
#make separate queries for each sprint to get higher/lower item right
-
56
return [] unless sprints
-
56
sprints.map do |s|
-
{ :sprint => s,
-
:stories => RbStory.backlog(project.id, s.id, nil, options)
-
181
}
-
end
-
end
-
-
1
def self.backlogs_by_release(project, releases, options={})
-
#make separate queries for each release to get higher/lower item right
-
55
return [] unless releases
-
55
releases.map do |r|
-
{ :release => r,
-
:stories => RbStory.backlog(project.id, nil, r.id, options)
-
4
}
-
end
-
end
-
-
1
def self.create_and_position(params)
-
654
params['prev'] = params.delete('prev_id') if params.include?('prev_id')
-
654
params['next'] = params.delete('next_id') if params.include?('next_id')
-
654
params['prev'] = nil if (['next', 'prev'] - params.keys).size == 2
-
-
# lft and rgt fields are handled by acts_as_nested_set
-
18968
attribs = params.select{|k,v| !['prev', 'next', 'id', 'lft', 'rgt'].include?(k) && RbStory.column_names.include?(k) }
-
654
attribs = Hash[*attribs.flatten]
-
654
s = RbStory.new(attribs)
-
654
s.save!
-
654
s.position!(params)
-
-
654
return s
-
end
-
-
1
scope :updated_since, lambda {|since|
-
18
where(["#{self.table_name}.updated_on > ?", Time.parse(since)]).
-
order("#{self.table_name}.updated_on ASC")
-
}
-
-
1
def self.find_all_updated_since(since, project_id)
-
#look in backlog, sprint and releases. look in shared sprints and shared releases
-
6
project = Project.select("id,lft,rgt").find_by_id(project_id)
-
18
sprints = project.open_shared_sprints.map{|s|s.id}
-
6
releases = project.open_releases_by_date.map{|s|s.id}
-
#following will execute 3 queries and join it as array
-
self.backlog_scope( {:project => project_id, :sprint => nil, :release => nil } ).
-
updated_since(since) |
-
self.backlog_scope( {:project => project_id, :sprint => sprints, :release => nil } ).
-
updated_since(since) |
-
self.backlog_scope( {:project => project_id, :sprint => nil, :release => releases } ).
-
6
updated_since(since)
-
end
-
-
1
def self.trackers(options = {})
-
# legacy
-
20133
options = {:type => options} if options.is_a?(Symbol)
-
-
# somewhere early in the initialization process during first-time migration this gets called when the table doesn't yet exist
-
20133
trackers = []
-
20133
if has_settings_table
-
20133
trackers = Backlogs.setting[:story_trackers]
-
20133
trackers = [] if trackers.blank?
-
end
-
-
20133
trackers = Tracker.find_all_by_id(trackers)
-
20133
trackers = trackers & options[:project].trackers if options[:project]
-
40266
trackers = trackers.sort_by { |t| [t.position] }
-
-
20133
case options[:type]
-
55
when :trackers then return trackers
-
40116
when :array, nil then return trackers.collect{|t| t.id}
-
40
when :string then return trackers.collect{|t| t.id.to_s}.join(',')
-
else raise "Unexpected return type #{options[:type].inspect}"
-
end
-
end
-
-
1
def self.has_settings_table
-
20133
ActiveRecord::Base.connection.tables.include?('settings')
-
end
-
-
1
def tasks
-
37
return self.children
-
end
-
-
1
def set_points(p)
-
return self.journalized_update_attribute(:story_points, nil) if p.blank? || p == '-'
-
-
return self.journalized_update_attribute(:story_points, 0) if p.downcase == 's'
-
-
return self.journalized_update_attribute(:story_points, Float(p)) if Float(p) >= 0
-
end
-
-
1
def points_display(notsized='-')
-
# For reasons I have yet to uncover, activerecord will
-
# sometimes return numbers as Fixnums that lack the nil?
-
# method. Comparing to nil should be safe.
-
3
return notsized if story_points == nil || story_points.blank?
-
1
return 'S' if story_points == 0
-
1
return story_points.to_s
-
end
-
-
1
def update_and_position!(params)
-
33
params['prev'] = params.delete('prev_id') if params.include?('prev_id')
-
33
params['next'] = params.delete('next_id') if params.include?('next_id')
-
33
self.position!(params)
-
-
# lft and rgt fields are handled by acts_as_nested_set
-
550
attribs = params.select{|k,v| !['prev', 'id', 'project_id', 'lft', 'rgt'].include?(k) && RbStory.column_names.include?(k) }
-
33
attribs = Hash[*attribs.flatten]
-
-
33
return self.journalized_update_attributes attribs
-
end
-
-
1
def position!(params)
-
687
if params.include?('prev')
-
654
if params['prev'].blank?
-
654
self.move_to_top # move after 'prev'. Meaning no prev, we go at top
-
else
-
self.move_after(RbStory.find(params['prev']))
-
end
-
33
elsif params.include?('next')
-
30
if params['next'].blank?
-
6
self.move_to_bottom
-
else
-
24
self.move_before(RbStory.find(params['next']))
-
end
-
end
-
end
-
-
# Produces relevant information for release graphs
-
# @param sprints is array of sprints of interest
-
# @return hash collection of
-
# :backlog_points :added_points :closed_points
-
# The dates are:
-
# start: first day of first sprint
-
# 1..n: a day after the nth sprint
-
1
def release_burndown_data(sprints)
-
16
return nil unless self.is_story?
-
16
days = Array.new
-
# Find interesting days of each sprint for the release graph
-
16
days << sprints.first.sprint_start_date.to_date
-
48
sprints.each { |sprint| days << sprint.effective_date.tomorrow.to_date }
-
-
16
baseline = [0] * days.size
-
-
16
series = Backlogs::MergedArray.new
-
16
series.merge(:backlog_points => baseline.dup)
-
16
series.merge(:added_points => baseline.dup)
-
16
series.merge(:closed_points => baseline.dup)
-
-
# Collect data
-
16
bd = {:points => [], :open => [], :accepted => [] }
-
16
self.history.filter_release(days).each{|d|
-
48
if d.nil? || d[:tracker] != :story
-
[:points, :open, :accepted].each{|k| bd[k] << nil }
-
else
-
48
bd[:points] << d[:story_points]
-
48
bd[:open] << d[:status_open]
-
48
bd[:accepted] << d[:status_success] #What do do with rejected points? The story is not open anymore.
-
end
-
}
-
-
16
series.merge(:accepted => bd[:accepted])
-
16
series.merge(:points => bd[:points])
-
16
series.merge(:open => bd[:open])
-
16
first = true;
-
16
series.merge(:accepted_first => series.series(:accepted).collect{ |a|
-
48
if a
-
8
if a == true && first == true
-
4
first = false
-
4
true
-
else
-
4
false
-
end
-
else
-
40
false
-
end
-
})
-
16
series.merge(:day => days)
-
-
# Extract added_points, backlog_points and closed points from the data collected
-
16
series.each { |p|
-
48
if (created_on.to_date < sprints.first.sprint_start_date.to_date) && p.open
-
36
p.backlog_points = p.points
-
end
-
48
if p.accepted_first
-
4
p.closed_points = p.points
-
end
-
# Is the story created within this sprint?
-
48
if (created_on.to_date >= sprints.first.sprint_start_date.to_date) &&
-
48
(created_on.to_date < p.day) #day is the end-date+1 of a sprint
-
p.added_points = p.points
-
if p.accepted
-
p.backlog_points = -p.points
-
end
-
end
-
}
-
-
16
rl = {}
-
16
rl[:backlog_points] = series.series(:backlog_points)
-
16
rl[:added_points] = series.series(:added_points)
-
16
rl[:closed_points] = series.series(:closed_points)
-
16
return rl
-
end
-
-
1
def burndown(sprint = nil, status=nil)
-
2384
return nil unless self.is_story?
-
2384
sprint ||= self.fixed_version.becomes(RbSprint) if self.fixed_version
-
2384
return nil if sprint.nil? || !sprint.has_burndown?
-
-
2384
bd = {:points_committed => [], :points_accepted => [], :points_resolved => [], :hours_remaining => []}
-
-
2384
self.history.filter(sprint, status).each{|d|
-
39493
if d.nil? || d[:sprint] != sprint.id || d[:tracker] != :story
-
189480
[:points_committed, :points_accepted, :points_resolved, :hours_remaining].each{|k| bd[k] << nil}
-
else
-
1597
bd[:points_committed] << d[:story_points]
-
1597
bd[:points_accepted] << (d[:status_success] ? d[:story_points] : 0)
-
1597
bd[:points_resolved] << (d[:status_success] || d[:hours].to_f == 0.0 ? d[:story_points] : 0)
-
1597
bd[:hours_remaining] << (d[:status_closed] ? 0 : d[:hours])
-
end
-
}
-
2384
return bd
-
end
-
-
1
def list_with_gaps_scope_condition(options={})
-
60
return options if self.new_record?
-
60
self.class.find_options(options.dup.merge({
-
:project => self.project_id,
-
:sprint => self.fixed_version_id,
-
:release => self.release_id
-
}))
-
end
-
-
1
def story_follow_task_state
-
26
return if Setting.plugin_redmine_backlogs[:story_follow_task_status] != 'close' && Setting.plugin_redmine_backlogs[:story_follow_task_status] != 'loose'
-
20
return if self.status.is_closed? #bail out if we are closed
-
-
20
self.reload #we might be stale at this point
-
20
case Setting.plugin_redmine_backlogs[:story_follow_task_status]
-
when 'close'
-
4
set_closed_status_if_following_to_close
-
when 'loose'
-
48
avg_ratio = tasks.map{|task| task.status.default_done_ratio.to_f }.sum / tasks.length # #837 coerce to float, nil counts for 0.0
-
#find status near avg_ratio
-
#find the status allowed, order by position, with nearest default_done_ratio not higher then avg_ratio
-
16
new_st = nil
-
16
self.new_statuses_allowed_to.each{|status|
-
53
new_st = status if status.default_done_ratio.to_f <= avg_ratio # #837 use to_f for comparison of number OR nil
-
53
break if status.default_done_ratio.to_f > avg_ratio
-
}
-
#set status and good.
-
16
self.journalized_update_attributes :status_id => new_st.id if new_st
-
16
set_closed_status_if_following_to_close
-
-
#calculate done_ratio weighted from tasks
-
16
recalculate_attributes_for(self.id) unless Issue.use_status_for_done_ratio?
-
else
-
-
end
-
end
-
-
1
def set_closed_status_if_following_to_close
-
20
status_id = Setting.plugin_redmine_backlogs[:story_close_status_id]
-
20
unless status_id.nil? || status_id.to_i == 0
-
# bail out if something is other than closed.
-
4
tasks.each{|task|
-
5
return unless task.status.is_closed?
-
}
-
1
self.journalized_update_attributes :status_id => status_id.to_i #update, but no need to position
-
end
-
end
-
end
-
1
require 'date'
-
-
1
class RbTask < Issue
-
1
unloadable
-
-
1
def self.tracker
-
8869
task_tracker = Backlogs.setting[:task_tracker]
-
8869
return nil if task_tracker.blank?
-
8869
return Integer(task_tracker)
-
end
-
-
# unify api between story and task. FIXME: remove this when merging to tracker-free-tasks
-
# required for RbServerVariablesHelper.workflow_transitions
-
1
def self.trackers
-
6
[self.tracker]
-
end
-
-
1
def self.rb_safe_attributes(params)
-
229
if Issue.const_defined? "SAFE_ATTRIBUTES"
-
safe_attributes_names = RbTask::SAFE_ATTRIBUTES
-
else
-
229
safe_attributes_names = Issue.new(
-
:project_id=>params[:project_id] # required to verify "safeness"
-
).safe_attribute_names
-
end
-
6652
attribs = params.select {|k,v| safe_attributes_names.include?(k) }
-
# lft and rgt fields are handled by acts_as_nested_set
-
4408
attribs = attribs.select{|k,v| k != 'lft' and k != 'rgt' }
-
229
attribs = Hash[*attribs.flatten] if attribs.is_a?(Array)
-
229
return attribs
-
end
-
-
1
def self.create_with_relationships(params, user_id, project_id, is_impediment = false)
-
202
attribs = rb_safe_attributes(params)
-
-
202
attribs['author_id'] = user_id
-
202
attribs['tracker_id'] = RbTask.tracker
-
202
attribs['project_id'] = project_id
-
-
202
blocks = params.delete('blocks')
-
-
#if we are an impediment and have blocks, set our project_id.
-
#if we have multiple blocked tasks, cross-project relations must be enabled, otherwise save-validation will fail. TODO: make this more user friendly by pre-validating here and suggesting to enable cross-project relation support in redmine base setup.
-
202
if is_impediment and blocks and blocks.strip != ''
-
80
begin
-
80
first_blocked_id = blocks.split(/\D+/)[0].to_i
-
80
attribs['project_id'] = Issue.find_by_id(first_blocked_id).project_id if first_blocked_id
-
rescue
-
end
-
end
-
-
202
task = new(attribs)
-
202
if params['parent_issue_id']
-
122
parent = Issue.find(params['parent_issue_id'])
-
122
task.start_date = parent.start_date
-
end
-
202
task.save!
-
-
202
raise "Block list must be comma-separated list of task IDs" if is_impediment && !task.validate_blocks_list(blocks) # could we do that before save and integrate cross-project checks?
-
-
202
task.move_before params[:next] unless is_impediment # impediments are not hosted under a single parent, so you can't tree-order them
-
202
task.update_blocked_list blocks.split(/\D+/) if is_impediment
-
202
task.time_entry_add(params)
-
-
202
return task
-
end
-
-
# TODO: there's an assumption here that impediments always have the
-
# task-tracker as their tracker, and are top-level issues.
-
1
def self.find_all_updated_since(since, project_id, find_impediments = false, sprint_id = nil)
-
#find all updated visible on taskboard - which may span projects.
-
3
if sprint_id.nil?
-
3
find(:all,
-
:conditions => ["project_id = ? AND updated_on > ? AND tracker_id in (?) and parent_id IS #{ find_impediments ? '' : 'NOT' } NULL", project_id, Time.parse(since), tracker],
-
:order => "updated_on ASC")
-
else
-
find(:all,
-
:conditions => ["fixed_version_id = ? AND updated_on > ? AND tracker_id in (?) and parent_id IS #{ find_impediments ? '' : 'NOT' } NULL", sprint_id, Time.parse(since), tracker],
-
:order => "updated_on ASC")
-
end
-
end
-
-
1
def update_with_relationships(params, is_impediment = false)
-
27
time_entry_add(params)
-
-
27
attribs = RbTask.rb_safe_attributes(params)
-
-
# Auto assign task to current user when
-
# 1. the task is not assigned to anyone yet
-
# 2. task status changed (i.e. Updating task name or remaining hours won't assign task to user)
-
# Can be enabled/disabled in setting page
-
27
if Backlogs.setting[:auto_assign_task] && self.assigned_to_id.blank? && (self.status_id != params[:status_id].to_i)
-
attribs[:assigned_to_id] = User.current.id
-
end
-
-
27
valid_relationships = if is_impediment && params[:blocks] #if blocks param was not sent, that means the impediment was just dragged
-
validate_blocks_list(params[:blocks])
-
else
-
27
true
-
end
-
-
27
if valid_relationships && result = self.journalized_update_attributes!(attribs)
-
27
move_before params[:next] unless is_impediment # impediments are not hosted under a single parent, so you can't tree-order them
-
27
update_blocked_list params[:blocks].split(/\D+/) if params[:blocks]
-
-
27
if params.has_key?(:remaining_hours)
-
25
begin
-
25
self.remaining_hours = Float(params[:remaining_hours].to_s.gsub(',', '.'))
-
rescue ArgumentError, TypeError
-
3
Rails.logger.warn "#{params[:remaining_hours]} is wrong format for remaining hours."
-
end
-
25
sprint_start = self.story.fixed_version.becomes(RbSprint).sprint_start_date if self.story
-
25
self.estimated_hours = self.remaining_hours if (sprint_start == nil) || (Date.today < sprint_start)
-
25
save
-
end
-
-
27
result
-
else
-
false
-
end
-
end
-
-
1
def update_blocked_list(for_blocking)
-
# Existing relationships not in for_blocking should be removed from the 'blocks' list
-
81
relations_from.find(:all, :conditions => "relation_type='blocks'").each{ |ir|
-
1
ir.destroy unless for_blocking.include?( ir[:issue_to_id] )
-
}
-
-
81
already_blocking = relations_from.find(:all, :conditions => "relation_type='blocks'").map{|ir| ir.issue_to_id}
-
-
# Non-existing relationships that are in for_blocking should be added to the 'blocks' list
-
162
for_blocking.select{ |id| !already_blocking.include?(id) }.each{ |id|
-
81
ir = relations_from.new(:relation_type=>'blocks')
-
81
ir[:issue_to_id] = id
-
81
ir.save!
-
}
-
81
reload
-
end
-
-
1
def validate_blocks_list(list)
-
80
if list.split(/\D+/).length==0
-
errors.add :blocks, :must_have_comma_delimited_list
-
false
-
else
-
80
true
-
end
-
end
-
-
# assumes the task is already under the same story as 'id'
-
1
def move_before(id)
-
149
id = nil if id.respond_to?('blank?') && id.blank?
-
149
if id.nil?
-
149
sib = self.siblings
-
149
move_to_right_of sib[-1].id if sib.any?
-
else
-
move_to_left_of id
-
end
-
end
-
-
1
def burndown(sprint = nil, status=nil)
-
sprint ||= self.fixed_version.becomes(RbSprint) if self.fixed_version
-
return nil if sprint.nil? || !sprint.has_burndown?
-
-
self.history.filter(sprint, status).collect{|d|
-
if d.nil? || d[:sprint] != sprint.id || d[:tracker] != :task
-
nil
-
elsif ! d[:status_open]
-
0
-
else
-
d[:hours]
-
end
-
}
-
end
-
-
1
def time_entry_add(params)
-
# Will also save time entry if only comment is filled, hours will default to 0. We don't want the user
-
# to loose a precious comment if hours is accidently left blank.
-
229
if !params[:time_entry_hours].blank? || !params[:time_entry_comments].blank?
-
@time_entry = TimeEntry.new(:issue => self, :project => self.project)
-
# Make sure user has permission to edit time entries to allow
-
# logging time for other users. Use current user in case none is selected
-
if User.current.allowed_to?(:edit_time_entries, self.project) && params[:time_entry_user_id].to_i != 0
-
@time_entry.user_id = params[:time_entry_user_id]
-
else
-
# Otherwise log time for current user
-
@time_entry.user_id = User.current.id
-
end
-
if !params[:time_entry_spent_on].blank?
-
@time_entry.spent_on = params[:time_entry_spent_on]
-
else
-
@time_entry.spent_on = Date.today
-
end
-
@time_entry.hours = params[:time_entry_hours].gsub(',', '.').to_f
-
# Choose default activity
-
# If default is not defined first activity will be chosen
-
if default_activity = TimeEntryActivity.default
-
@time_entry.activity_id = default_activity.id
-
else
-
@time_entry.activity_id = TimeEntryActivity.first.id
-
end
-
@time_entry.comments = params[:time_entry_comments]
-
self.time_entries << @time_entry
-
end
-
end
-
end
-
1
require 'redmine'
-
-
1
if Rails::VERSION::MAJOR < 3
-
require 'dispatcher'
-
object_to_prepare = Dispatcher
-
else
-
1
object_to_prepare = Rails.configuration
-
# if redmine plugins were railties:
-
# object_to_prepare = config
-
end
-
1
object_to_prepare.to_prepare do
-
1
require_dependency 'backlogs_activerecord_mixin'
-
1
require_dependency 'backlogs_setup'
-
1
require_dependency 'issue'
-
-
1
if Issue.const_defined? "SAFE_ATTRIBUTES"
-
Issue::SAFE_ATTRIBUTES << "story_points"
-
Issue::SAFE_ATTRIBUTES << "position"
-
Issue::SAFE_ATTRIBUTES << "remaining_hours"
-
else
-
1
Issue.safe_attributes "story_points", "position", "remaining_hours"
-
end
-
-
1
require_dependency 'backlogs_query_patch'
-
1
require_dependency 'backlogs_issue_patch'
-
1
require_dependency 'backlogs_issue_status_patch'
-
1
require_dependency 'backlogs_tracker_patch'
-
1
require_dependency 'backlogs_version_patch'
-
1
require_dependency 'backlogs_project_patch'
-
1
require_dependency 'backlogs_user_patch'
-
1
require_dependency 'backlogs_custom_field_patch'
-
-
1
require_dependency 'backlogs_my_controller_patch'
-
1
require_dependency 'backlogs_issues_controller_patch'
-
1
require_dependency 'backlogs_projects_helper_patch'
-
-
1
require_dependency 'backlogs_hooks'
-
-
1
require_dependency 'backlogs_merged_array'
-
-
1
require_dependency 'backlogs_printable_cards'
-
-
1
Redmine::AccessControl.permission(:manage_versions).actions << "rb_sprints/close_completed"
-
end
-
-
-
1
Redmine::Plugin.register :redmine_backlogs do
-
1
name 'Redmine Backlogs'
-
1
author "friflaj,Mark Maglana,John Yani,mikoto20000,Frank Blendinger,Bo Hansen,stevel,Patrick Atamaniuk"
-
1
description 'A plugin for agile teams'
-
1
version 'v0.9.36'
-
-
settings :default => {
-
:story_trackers => nil,
-
:task_tracker => nil,
-
:card_spec => nil,
-
:story_close_status_id => '0',
-
:taskboard_card_order => 'story_follows_tasks',
-
:story_points => "1,2,3,5,8",
-
:show_burndown_in_sidebar => 'enabled',
-
:show_project_name => nil,
-
:scrum_stats_menu_position => 'top'
-
},
-
1
:partial => 'backlogs/settings'
-
-
1
project_module :backlogs do
-
# SYNTAX: permission :name_of_permission, { :controller_name => [:action1, :action2] }
-
-
# Master backlog permissions
-
1
permission :reset_sprint, {
-
:rb_sprints => :reset
-
}
-
1
permission :configure_backlogs, { :rb_project_settings => :project_settings }
-
1
permission :view_master_backlog, {
-
:rb_master_backlogs => [:show, :menu, :closed_sprints],
-
:rb_sprints => [:index, :show, :download],
-
:rb_hooks_render => [:view_issues_sidebar],
-
:rb_wikis => :show,
-
:rb_stories => [:index, :show, :tooltip],
-
:rb_queries => [:show, :impediments],
-
:rb_server_variables => [:project, :sprint, :index],
-
:rb_burndown_charts => [:embedded, :show, :print],
-
:rb_updated_items => :show
-
}
-
-
1
permission :view_releases, {
-
:rb_releases => [:index, :show],
-
:rb_sprints => [:index, :show, :download],
-
:rb_wikis => :show,
-
:rb_stories => [:index, :show, :tooltip],
-
:rb_server_variables => [:project, :sprint, :index],
-
:rb_burndown_charts => [:embedded, :show, :print],
-
:rb_updated_items => :show
-
}
-
-
1
permission :view_taskboards, {
-
:rb_taskboards => [:current, :show],
-
:rb_sprints => :show,
-
:rb_stories => [:index, :show, :tooltip],
-
:rb_tasks => [:index, :show],
-
:rb_impediments => [:index, :show],
-
:rb_wikis => :show,
-
:rb_server_variables => [:project, :sprint, :index],
-
:rb_hooks_render => [:view_issues_sidebar],
-
:rb_burndown_charts => [:embedded, :show, :print],
-
:rb_updated_items => :show
-
}
-
-
# Release permissions
-
1
permission :modify_releases, { :rb_releases => [:new, :create, :edit, :update, :snapshot, :destroy] }
-
-
# Sprint permissions
-
# :show_sprints and :list_sprints are implicit in :view_master_backlog permission
-
1
permission :create_sprints, { :rb_sprints => [:new, :create] }
-
1
permission :update_sprints, {
-
:rb_sprints => [:edit, :update],
-
:rb_wikis => [:edit, :update]
-
}
-
-
# Story permissions
-
# :show_stories and :list_stories are implicit in :view_master_backlog permission
-
1
permission :create_stories, { :rb_stories => :create }
-
1
permission :update_stories, { :rb_stories => :update }
-
-
# Task permissions
-
# :show_tasks and :list_tasks are implicit in :view_sprints
-
1
permission :create_tasks, { :rb_tasks => [:new, :create] }
-
1
permission :update_tasks, { :rb_tasks => [:edit, :update] }
-
-
1
permission :update_remaining_hours, { :rb_tasks => [:edit, :update] }
-
-
# Impediment permissions
-
# :show_impediments and :list_impediments are implicit in :view_sprints
-
1
permission :create_impediments, { :rb_impediments => [:new, :create] }
-
1
permission :update_impediments, { :rb_impediments => [:edit, :update] }
-
-
1
permission :subscribe_to_calendars, { :rb_calendars => :ical }
-
1
permission :view_scrum_statistics, { :rb_all_projects => :statistics }
-
end
-
-
124
menu :project_menu, :rb_master_backlogs, { :controller => :rb_master_backlogs, :action => :show }, :caption => :label_backlogs, :after => :roadmap, :param => :project_id, :if => Proc.new { Backlogs.configured? }
-
124
menu :project_menu, :rb_taskboards, { :controller => :rb_taskboards, :action => :current }, :caption => :label_task_board, :after => :rb_master_backlogs, :param => :project_id, :if => Proc.new {|project| Backlogs.configured? && project && project.active_sprint }
-
124
menu :project_menu, :rb_releases, { :controller => :rb_releases, :action => :index }, :caption => :label_release_plural, :after => :rb_taskboards, :param => :project_id, :if => Proc.new { Backlogs.configured? }
-
-
1
menu :top_menu, :rb_statistics, { :controller => :rb_all_projects, :action => :statistics}, :caption => :label_scrum_statistics,
-
:if => Proc.new {
-
Backlogs.configured? &&
-
374
User.current.allowed_to?({:controller => :rb_all_projects, :action => :statistics}, nil, :global => true) &&
-
99
(Backlogs.setting[:scrum_stats_menu_position].nil? || Backlogs.setting[:scrum_stats_menu_position] == 'top')
-
}
-
1
menu :application_menu, :rb_statistics, { :controller => :rb_all_projects, :action => :statistics}, :caption => :label_scrum_statistics,
-
:if => Proc.new {
-
Backlogs.configured? &&
-
251
User.current.allowed_to?({:controller => :rb_all_projects, :action => :statistics}, nil, :global => true) &&
-
Backlogs.setting[:scrum_stats_menu_position] == 'application'
-
}
-
end
-
1
module Backlogs
-
1
module ActiveRecord
-
1
def add_condition(options, condition, conjunction = 'AND')
-
697
if condition.is_a? String
-
add_condition(options, [condition], conjunction)
-
697
elsif condition.is_a? Hash
-
add_condition!(options, [condition.keys.map { |attr| "#{attr}=?" }.join(' AND ')] + condition.values, conjunction)
-
697
elsif condition.is_a? Array
-
697
options[:conditions] ||= []
-
697
options[:conditions][0] += " #{conjunction} (#{condition.shift})" unless options[:conditions].empty?
-
697
options[:conditions] = options[:conditions] + condition
-
else
-
raise "don't know how to handle this condition type"
-
end
-
end
-
1
module_function :add_condition
-
-
1
module Attributes
-
1
def self.included receiver
-
4
receiver.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def rb_sti_class
-
62136
return self.ancestors.select{|klass| klass.name !~ /^Rb/ && klass.ancestors.include?(::ActiveRecord::Base)}[0]
-
end
-
end
-
-
1
def available_custom_fields
-
696
klass = self.class.respond_to?(:rb_sti_class) ? self.class.rb_sti_class : self.class
-
696
CustomField.find(:all, :conditions => "type = '#{klass.name}CustomField'", :order => 'position')
-
end
-
-
1
def journalized_update_attributes!(attribs)
-
27
self.init_journal(User.current)
-
27
return self.update_attributes!(attribs)
-
end
-
1
def journalized_update_attributes(attribs)
-
50
self.init_journal(User.current)
-
50
return self.update_attributes(attribs)
-
end
-
1
def journalized_update_attribute(attrib, v)
-
1
self.init_journal(User.current)
-
1
return self.update_attribute(attrib, v)
-
end
-
end
-
-
1
module ListWithGaps
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
1
def acts_as_list_with_gaps(options={})
-
1
options[:spacing] ||= 50
-
1
options[:default] ||= :top
-
-
class_eval <<-EOV
-
include Backlogs::ActiveRecord::ListWithGaps::InstanceMethods
-
-
def self.list_spacing
-
#{options[:spacing]}
-
end
-
-
def self.find_by_rank(r, options)
-
self.find(:first, options.merge(:order => '#{self.table_name}.position', :limit => 1, :offset => r - 1))
-
end
-
-
before_create :move_to_#{options[:default]}
-
1
EOV
-
end
-
end
-
-
1
module InstanceMethods
-
1
def move_to_top(options={})
-
2380
top = self.class.minimum(:position)
-
2380
return if self.position == top && !top.blank?
-
1726
self.position = top.blank? ? 0 : (top - self.class.list_spacing)
-
1726
list_commit
-
end
-
-
1
def move_to_bottom(options={})
-
660
bottom = self.class.maximum(:position)
-
660
return if self.position == bottom && !bottom.blank?
-
596
self.position = bottom.blank? ? 0 : (bottom + self.class.list_spacing)
-
596
list_commit
-
end
-
-
1
def first(options = {})
-
return self.class.find_by_position(self.class.minimum(:position, options))
-
end
-
-
1
def last(options = {})
-
return self.class.find_by_position(self.class.maximum(:position, options))
-
end
-
-
1
def higher_item(options={})
-
60
@higher_item ||= list_prev_next(:prev, self.list_with_gaps_scope_condition(options))
-
end
-
1
attr_writer :higher_item
-
-
1
def lower_item(options={})
-
28
@lower_item ||= list_prev_next(:next, self.list_with_gaps_scope_condition(options))
-
end
-
1
attr_writer :lower_item
-
-
# higher_item and lower_item use this scope condition to determine neighbours
-
# to be overloaded
-
1
def list_with_gaps_scope_condition(options={})
-
options
-
end
-
-
1
def rank
-
@rank ||= self.class.
-
scoped(self.list_with_gaps_scope_condition).
-
where(["#{self.class.table_name}.position <= ?", self.position]).
-
6
count
-
end
-
1
attr_writer :rank
-
-
1
def move_after(reference, options={})
-
2
nxt = reference.send(:lower_item_unscoped)
-
-
2
if nxt.blank?
-
1
move_to_bottom
-
else
-
1
if (nxt.position - reference.position) < 2
-
self.class.connection.execute("update #{self.class.table_name} set position = position + #{self.class.list_spacing} where position >= #{nxt.position}")
-
nxt.position += self.class.list_spacing
-
end
-
1
self.position = (nxt.position + reference.position) / 2
-
end
-
-
2
list_commit
-
end
-
-
#issues are listed by position ascending, which is in rank descending. Higher means lower position
-
#before means lower position
-
1
def move_before(reference, options={})
-
26
prev = reference.send(:higher_item_unscoped)
-
-
26
if prev.blank?
-
2
move_to_top
-
else
-
24
if (reference.position - prev.position) < 2
-
2
self.class.connection.execute("update #{self.class.table_name} set position = position - #{self.class.list_spacing} where position <= #{prev.position}")
-
2
prev.position -= self.class.list_spacing
-
end
-
24
self.position = (reference.position + prev.position) / 2
-
end
-
-
26
list_commit
-
end
-
-
end
-
-
1
private
-
-
#higher item is the one with lower position. self is visually displayed below its higher item.
-
1
def higher_item_unscoped(options = {})
-
33
@higher_item_unscoped ||= list_prev_next(:prev, options)
-
end
-
-
1
def lower_item_unscoped(options = {})
-
9
@lower_item_unscoped ||= list_prev_next(:next, options)
-
end
-
-
1
def list_commit
-
2350
self.class.connection.execute("update #{self.class.table_name} set position = #{self.position} where id = #{self.id}") unless self.new_record?
-
#FIXME now the cached lower/higher_item are wrong during this request. So are those from our old and new peers.
-
end
-
-
1
def list_prev_next(dir, options)
-
96
return nil if self.new_record?
-
96
raise "#{self.class}##{self.id}: cannot request #{dir} for nil position" unless self.position
-
96
options = options.dup
-
96
Backlogs::ActiveRecord.add_condition(options, ["#{self.class.table_name}.position #{dir == :prev ? '<' : '>'} ?", self.position])
-
96
options[:order] = "#{self.class.table_name}.position #{dir == :prev ? 'desc' : 'asc'}"
-
96
return self.class.find(:first, options)
-
end
-
-
end
-
end
-
end
-
-
1
ActiveRecord::Base.send(:include, Backlogs::ActiveRecord::ListWithGaps) unless ActiveRecord::Base.included_modules.include? Backlogs::ActiveRecord::ListWithGaps
-
1
require_dependency 'custom_field'
-
-
1
module Backlogs
-
1
module CustomFieldPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
1
base.class_eval do
-
1
class << self
-
1
alias_method_chain :customized_class, :sti
-
end
-
end
-
end
-
-
1
module ClassMethods
-
1
def customized_class_with_sti
-
712
(self.respond_to?(:rb_sti_class) ? self.rb_sti_class : self).customized_class_without_sti
-
end
-
end
-
-
1
module InstanceMethods
-
end
-
end
-
end
-
-
1
CustomField.send(:include, Backlogs::CustomFieldPatch) unless CustomField.included_modules.include? Backlogs::CustomFieldPatch
-
1
include RbCommonHelper
-
1
include ContextMenusHelper
-
-
1
module BacklogsPlugin
-
1
module Hooks
-
1
class LayoutHook < Redmine::Hook::ViewListener
-
# this ought to be view_issues_sidebar_queries_bottom, but
-
# the entire queries toolbar is disabled if you don't have
-
# custom queries
-
-
1
def exception(context, ex)
-
context[:controller].send(:flash)[:error] = "Backlogs error: #{ex.message} (#{ex.class})"
-
Rails.logger.error "#{ex.message} (#{ex.class}): " + ex.backtrace.join("\n")
-
end
-
-
1
def view_issues_sidebar_planning_bottom(context={ })
-
19
begin
-
19
return '' if User.current.anonymous?
-
-
19
project = context[:project]
-
-
19
return '' unless project && !project.blank?
-
19
return '' unless Backlogs.configured?(project)
-
-
19
sprint_id = nil
-
-
19
params = context[:controller].params
-
19
case "#{params['controller']}##{params['action']}"
-
when 'issues#show'
-
3
if params['id'] && (issue = Issue.find(params['id'])) && (issue.is_task? || issue.is_story?) && issue.fixed_version
-
3
sprint_id = issue.fixed_version_id
-
end
-
-
when 'issues#index'
-
16
q = context[:request].session[:query]
-
16
sprint = (q && q[:filters]) ? q[:filters]['fixed_version_id'] : nil
-
16
if sprint && sprint[:operator] == '=' && sprint[:values].size == 1
-
7
sprint_id = sprint[:values][0]
-
end
-
end
-
-
19
url_options = {
-
:only_path => true,
-
:controller => :rb_hooks_render,
-
:action => :view_issues_sidebar,
-
:project_id => project.identifier
-
}
-
19
url_options[:sprint_id] = sprint_id if sprint_id
-
19
url = url_for_prefix_in_hooks
-
19
url += url_for(url_options)
-
-
# Why can't I access protect_against_forgery?
-
return %{
-
<div id="backlogs_view_issues_sidebar"></div>
-
<script type="text/javascript">
-
var $j = RB.$ || $;
-
$j(function($) {
-
19
$('#backlogs_view_issues_sidebar').load('#{url}');
-
});
-
</script>
-
}
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
end
-
-
1
def view_issues_show_details_bottom(context={ })
-
3
begin
-
3
issue = context[:issue]
-
-
3
return '' unless Backlogs.configured?(issue.project)
-
-
3
snippet = ''
-
-
3
project = context[:project]
-
-
3
if issue.is_story?
-
3
snippet += "<tr><th>#{l(:field_story_points)}</th><td>#{RbStory.find(issue.id).points_display}</td>"
-
3
unless issue.remaining_hours.nil?
-
3
snippet += "<th>#{l(:field_remaining_hours)}</th><td>#{l_hours(issue.remaining_hours)}</td>"
-
end
-
3
snippet += "</tr>"
-
3
vbe = issue.velocity_based_estimate
-
3
snippet += "<tr><th>#{l(:field_velocity_based_estimate)}</th><td>#{vbe ? vbe.to_s + ' days' : '-'}</td></tr>"
-
-
3
unless issue.release_id.nil?
-
release = RbRelease.find(issue.release_id)
-
snippet += "<tr><th>#{l(:field_release)}</th><td>#{link_to release.name, :controller=>'rb_releases', :action=>'show', :release_id=>release}</td></tr>"
-
end
-
end
-
-
3
if issue.is_task? && User.current.allowed_to?(:update_remaining_hours, project) != nil
-
snippet += "<tr><th>#{l(:field_remaining_hours)}</th><td>#{issue.remaining_hours}</td></tr>"
-
end
-
-
3
return snippet
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
end
-
-
1
def view_issues_form_details_bottom(context={ })
-
5
begin
-
5
snippet = ''
-
5
issue = context[:issue]
-
-
5
return '' unless Backlogs.configured?(issue.project)
-
-
#project = context[:project]
-
-
#developers = project.members.select {|m| m.user.allowed_to?(:log_time, project)}.collect{|m| m.user}
-
#developers = select_tag("time_entry[user_id]", options_from_collection_for_select(developers, :id, :name, User.current.id))
-
#developers = developers.gsub(/\n/, '')
-
-
5
if issue.is_story?
-
5
snippet += '<p>'
-
#snippet += context[:form].label(:story_points)
-
5
snippet += context[:form].text_field(:story_points, :size => 3)
-
5
snippet += '</p>'
-
-
5
if issue.safe_attribute?('release_id') && issue.assignable_releases.any?
-
snippet += '<p>'
-
snippet += context[:form].select :release_id, release_options_for_select(issue.assignable_releases, issue.release), :include_blank => true
-
snippet += '</p>'
-
end
-
-
5
if issue.descendants.length != 0 && !issue.new_record?
-
3
snippet += <<-generatedscript
-
-
<script type="text/javascript">
-
var $j = RB.$ || $;
-
$j(function($) {
-
$('#issue_estimated_hours').attr('disabled', 'disabled');
-
$('#issue_done_ratio').attr('disabled', 'disabled');
-
$('#issue_start_date').parent().hide();
-
$('#issue_due_date').parent().hide();
-
});
-
</script>
-
generatedscript
-
end
-
end
-
-
5
params = context[:controller].params
-
5
if issue.is_story? && params[:copy_from]
-
2
snippet += "<p><label for='link_to_original'>#{l(:rb_label_link_to_original)}</label>"
-
2
snippet += "#{check_box_tag('link_to_original', params[:copy_from], true)}</p>"
-
-
2
snippet += "<p><label>#{l(:rb_label_copy_tasks)}</label>"
-
2
snippet += "#{radio_button_tag('copy_tasks', 'open:' + params[:copy_from], true)} #{l(:rb_label_copy_tasks_open)}<br />"
-
2
snippet += "#{radio_button_tag('copy_tasks', 'none', false)} #{l(:rb_label_copy_tasks_none)}<br />"
-
2
snippet += "#{radio_button_tag('copy_tasks', 'all:' + params[:copy_from], false)} #{l(:rb_label_copy_tasks_all)}</p>"
-
end
-
-
5
if issue.is_task? && !issue.new_record?
-
snippet += "<p><label for='remaining_hours'>#{l(:field_remaining_hours)}</label>"
-
snippet += text_field_tag('remaining_hours', issue.remaining_hours, :size => 3)
-
snippet += '</p>'
-
end
-
-
5
return snippet
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
end
-
-
1
def view_issues_new_top(context={ })
-
#Remove the copy_subtasks functionality from redmine 2.1+ since backlogs offers it with a choice to copy only open tasks
-
2
project = context[:project]
-
2
return '' unless project.module_enabled?('backlogs')
-
2
return '<script type="text/javascript">$(function(){try{$("#copy_subtasks")[0].checked=false;$($("#copy_subtasks")[0].parentNode).hide();}catch(e){}});</script>' if (Redmine::VERSION::MAJOR == 2 && Redmine::VERSION::MINOR >= 1) || Redmine::VERSION::MAJOR > 2
-
end
-
-
1
def view_issues_bulk_edit_details_bottom(context={ })
-
project = context[:project]
-
return if project.nil? #hmm, this might happen when issues of different projects are bulk-edited
-
#what we could do is to intersect issues.each{ issue.assignable_releases }
-
return unless project.releases && project.releases.size > 0 #no releases in this project. #FIXME: sharing releases
-
snippet = ''
-
snippet += "<p>
-
<label for='issue_release_id'>#{ l(:field_release)}</label>
-
#{ select_tag('issue[release_id]', content_tag('option', l(:label_no_change_option), :value => '') +
-
content_tag('option', l(:label_none), :value => 'none') +
-
release_options_for_select(project.releases)) }
-
</p>"
-
end
-
-
1
def view_issues_context_menu_end(context={ })
-
begin
-
issues = context[:issues]
-
issue = nil
-
issue = issues.first if issues.size == 1
-
projects = issues.collect(&:project).compact.uniq
-
return if projects.size == 0
-
releases = projects.map {|p| p.shared_releases.open}.reduce(:&)
-
return if releases.size == 0
-
snippet='
-
<li class="folder">
-
<a href="#" class="submenu">'+ l(:field_release) + '</a>
-
<ul>'
-
releases.each do |s|
-
snippet += '<li>' +
-
context_menu_link(s.name,
-
{:controller => 'issues', :action => 'bulk_update', :ids => issues, :issue => {:release_id => s}, :back_url => context[:back]},
-
:method => :post,
-
:selected => (issue && s == issue.release),
-
:disabled => !context[:can][:update])+
-
'</li>'
-
end
-
snippet += '<li>' +
-
context_menu_link(l(:label_none),
-
{:controller => 'issues', :action => 'bulk_update', :ids => issues, :issue => {:release_id => 'none'}, :back_url => context[:back]},
-
:method => :post,
-
:selected => (issue && issue.release.nil?),
-
:disabled => !context[:can][:update])+
-
'</li>'
-
snippet += '
-
</ul>
-
</li>'
-
rescue => e
-
Rails.logger.error("Exception in Backlogs view_issues_context_menu_end #{e}")#exception(context, e)
-
return ''
-
end
-
end
-
-
1
def view_versions_show_bottom(context={ })
-
1
begin
-
1
version = context[:version]
-
1
project = version.project
-
-
1
return '' unless Backlogs.configured?(project)
-
-
1
snippet = ''
-
-
1
if User.current.allowed_to?(:edit_wiki_pages, project)
-
1
snippet += '<span id="edit_wiki_page_action">'
-
1
snippet += link_to l(:button_edit_wiki),
-
url_for_prefix_in_hooks + url_for({:controller => 'rb_wikis', :action => 'edit', :sprint_id => version.id }),
-
:class => 'icon icon-edit'
-
1
snippet += '</span>'
-
-
# this wouldn't be necesary if the schedules plugin
-
# didn't disable the contextual hook
-
1
snippet += <<-generatedscript
-
-
<script type="text/javascript">
-
var $j = RB.$ || $;
-
$j(function($) {
-
$('#edit_wiki_page_action').detach().appendTo("div.contextual");
-
//hide the redmine 'edit associated wiki page' if it exists, so we only have our button consistently.
-
if ('#{version.wiki_page_title}' !== '') $(".contextual a.icon-edit:contains('#{version.wiki_page_title}')").hide()
-
});
-
</script>
-
generatedscript
-
end
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
end
-
-
1
def view_my_account(context={ })
-
begin
-
return %{
-
</fieldset>
-
<fieldset class="box tabular">
-
<h3>#{l(:label_backlogs)}</h3>
-
<p>
-
#{label :backlogs, :task_color}
-
#{text_field :backlogs, :task_color, :value => context[:user].backlogs_preference[:task_color]}
-
</p>
-
}
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
end
-
-
1
def controller_issues_new_after_save(context={ })
-
2
params = context[:params]
-
2
issue = context[:issue]
-
-
2
return unless Backlogs.configured?(issue.project)
-
-
2
if issue.is_story?
-
2
if params[:link_to_original]
-
2
rel = IssueRelation.new
-
-
2
rel.issue_from_id = Integer(params[:link_to_original])
-
2
rel.issue_to_id = issue.id
-
2
rel.relation_type = IssueRelation::TYPE_RELATES
-
2
rel.save
-
end
-
-
2
if params[:copy_tasks] =~ /^[a-z]+:[0-9]+$/
-
1
action, id = *(params[:copy_tasks].split(/:/))
-
-
1
story = RbStory.find(Integer(id))
-
-
1
if action != 'none'
-
1
case action
-
when 'open'
-
3
tasks = story.tasks.select{|t| !t.reload.closed?}
-
when 'none'
-
tasks = []
-
when 'all'
-
tasks = story.tasks
-
else
-
raise "Unexpected value #{params[:copy_tasks]}"
-
end
-
-
1
tasks.each {|t|
-
2
nt = Issue.new
-
2
nt.copy_from(t)
-
2
nt.parent_issue_id = issue.id
-
2
nt.position = nil # will assign a new position
-
2
nt.save!
-
}
-
end
-
end
-
end
-
end
-
-
1
def controller_issues_edit_before_save(context={ })
-
params = context[:params]
-
issue = context[:issue]
-
-
if issue.is_task?
-
begin
-
issue.remaining_hours = Float(params[:remaining_hours])
-
rescue ArgumentError, TypeError
-
issue.remaining_hours = nil
-
end
-
end
-
end
-
-
1
def view_layouts_base_html_head(context={})
-
467
return '' if Setting.login_required? && !User.current.logged?
-
-
467
if User.current.admin? && !context[:request].session[:backlogs_configured]
-
5
context[:request].session[:backlogs] = Backlogs.configured?
-
5
unless context[:request].session[:backlogs]
-
context[:controller].send(:flash)[:error] = l(:label_backlogs_unconfigured, {:administration => l(:label_administration), :plugins => l(:label_plugins), :configure => l(:button_configure)})
-
end
-
end
-
-
467
return context[:controller].send(:render_to_string, {
-
:locals => context,
-
:partial=> 'hooks/rb_include_scripts'})
-
end
-
-
1
def view_timelog_edit_form_bottom(context={ })
-
2
time_entry = context[:time_entry]
-
2
return '' if time_entry[:issue_id].blank?
-
-
1
issue = Issue.find(context[:time_entry].issue_id)
-
return '' unless Backlogs.configured?(issue.project) &&
-
1
Backlogs.setting[:timelog_from_taskboard]=='enabled'
-
1
snippet=''
-
-
1
begin
-
1
if issue.is_task? && User.current.allowed_to?(:update_remaining_hours, time_entry.project) != nil
-
1
remaining_hours = issue.remaining_hours
-
1
snippet += "<p><label for='remaining_hours'>#{l(:field_remaining_hours)}</label>"
-
1
snippet += text_field_tag('remaining_hours', remaining_hours, :size => 6)
-
1
snippet += '</p>'
-
end
-
1
return snippet
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
-
end
-
-
1
def controller_timelog_edit_before_save(context={ })
-
2
time_entry = context[:time_entry]
-
2
return '' if time_entry[:issue_id].blank?
-
-
1
params = context[:params]
-
-
1
issue = Issue.find(time_entry.issue_id)
-
return unless Backlogs.configured?(issue.project) &&
-
1
Backlogs.setting[:timelog_from_taskboard]=='enabled'
-
-
1
if issue.is_task? && User.current.allowed_to?(:update_remaining_hours, time_entry.project) != nil
-
1
if params.include?("remaining_hours")
-
1
remaining_hours = params[:remaining_hours].gsub(',','.').to_f
-
1
if remaining_hours != issue.remaining_hours
-
1
issue.journalized_update_attribute(:remaining_hours, remaining_hours) if time_entry.save
-
end
-
end
-
end
-
end
-
-
end
-
end
-
end
-
1
require_dependency 'issue'
-
-
1
module Backlogs
-
1
module IssuePatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
1
base.class_eval do
-
1
unloadable
-
-
1
belongs_to :release, :class_name => 'RbRelease', :foreign_key => 'release_id'
-
-
1
acts_as_list_with_gaps :default => (Backlogs.setting[:new_story_position] == 'bottom' ? 'bottom' : 'top')
-
-
1
has_one :backlogs_history, :class_name => RbIssueHistory, :dependent => :destroy
-
-
1
safe_attributes 'release_id' #FIXME merge conflict. is this required?
-
-
1
before_save :backlogs_before_save
-
1
after_save :backlogs_after_save
-
-
1
include Backlogs::ActiveRecord::Attributes
-
end
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def history
-
4713
@history ||= RbIssueHistory.find_or_create_by_issue_id(self.id)
-
end
-
-
1
def is_story?
-
5233
return RbStory.trackers.include?(tracker_id)
-
end
-
-
1
def is_task?
-
1601
return (tracker_id == RbTask.tracker)
-
end
-
-
1
def story
-
2315
if @rb_story.nil?
-
1565
if self.new_record?
-
900
parent_id = self.parent_id
-
900
parent_id = self.parent_issue_id if parent_id.blank?
-
900
parent_id = nil if parent_id.blank?
-
900
parent = parent_id ? Issue.find(parent_id) : nil
-
-
900
if parent.nil?
-
782
@rb_story = nil
-
elsif parent.is_story?
-
118
@rb_story = parent.becomes(RbStory)
-
else
-
@rb_story = parent.story
-
end
-
else
-
665
@rb_story = Issue.find(:first, :order => 'lft DESC', :conditions => [ "root_id = ? and lft < ? and rgt > ? and tracker_id in (?)", root_id, lft, rgt, RbStory.trackers ])
-
665
@rb_story = @rb_story.becomes(RbStory) if @rb_story
-
end
-
end
-
2315
return @rb_story
-
end
-
-
1
def blocks
-
# return issues that I block that aren't closed
-
87
return [] if closed?
-
87
begin
-
136
return relations_from.collect {|ir| ir.relation_type == 'blocks' && !ir.issue_to.closed? ? ir.issue_to : nil }.compact
-
rescue
-
# stupid rails and their ignorance of proper relational databases
-
Rails.logger.error "Cannot return the blocks list for #{self.id}: #{e}"
-
return []
-
end
-
end
-
-
1
def blockers
-
# return issues that block me
-
return [] if closed?
-
relations_to.collect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed? ? ir.issue_from : nil}.compact
-
end
-
-
1
def velocity_based_estimate
-
3
return nil if !self.is_story? || ! self.story_points || self.story_points <= 0
-
-
1
hpp = self.project.scrum_statistics.hours_per_point
-
1
return nil if ! hpp
-
-
return Integer(self.story_points * (hpp / 8))
-
end
-
-
1
def backlogs_before_save
-
1581
if Backlogs.configured?(project)
-
1581
if (self.is_task? || self.story)
-
475
self.remaining_hours = self.estimated_hours if self.remaining_hours.blank?
-
475
self.estimated_hours = self.remaining_hours if self.estimated_hours.blank?
-
-
475
self.remaining_hours = 0 if self.status.backlog_is?(:success)
-
-
475
self.fixed_version = self.story.fixed_version if self.story
-
475
self.start_date = Date.today if self.start_date.blank? && self.status_id != IssueStatus.default.id
-
-
475
self.tracker = Tracker.find(RbTask.tracker) unless self.tracker_id == RbTask.tracker
-
elsif self.is_story? && Backlogs.setting[:set_start_and_duedates_from_sprint]
-
if self.fixed_version
-
self.start_date ||= (self.fixed_version.sprint_start_date || Date.today)
-
self.due_date ||= self.fixed_version.effective_date
-
self.due_date = self.start_date if self.due_date && self.due_date < self.start_date
-
else
-
self.start_date = nil
-
self.due_date = nil
-
end
-
end
-
end
-
1581
self.remaining_hours = self.leaves.sum("COALESCE(remaining_hours, 0)").to_f unless self.leaves.empty?
-
-
1581
self.move_to_top if self.position.blank? || (@copied_from.present? && @copied_from.position == self.position)
-
-
# scrub position from the journal by copying the new value to the old
-
1581
@attributes_before_change['position'] = self.position if @attributes_before_change
-
-
1581
@backlogs_new_record = self.new_record?
-
-
1581
return true
-
end
-
-
1
def backlogs_after_save
-
1581
self.history.save!
-
1581
[self.parent_id, self.parent_id_was].compact.uniq.each{|pid|
-
385
p = Issue.find(pid)
-
385
r = p.leaves.sum("COALESCE(remaining_hours, 0)").to_f
-
385
if r != p.remaining_hours
-
p.update_attribute(:remaining_hours, r)
-
p.history.save
-
end
-
}
-
-
1581
return unless Backlogs.configured?(self.project)
-
-
1581
if self.is_story?
-
# raw sql and manual journal here because not
-
# doing so causes an update loop when Issue calls
-
# update_parent :<
-
1106
tasklist = RbTask.find(:all, :conditions => ["root_id=? and lft>? and rgt<? and
-
(
-
(? is NULL and not fixed_version_id is NULL)
-
or
-
(not ? is NULL and fixed_version_id is NULL)
-
or
-
(not ? is NULL and not fixed_version_id is NULL and ?<>fixed_version_id)
-
or
-
(tracker_id <> ?)
-
)", self.root_id, self.lft, self.rgt,
-
self.fixed_version_id, self.fixed_version_id,
-
self.fixed_version_id, self.fixed_version_id,
-
RbTask.tracker]).to_a
-
1114
tasklist.each{|task| task.history.save! }
-
1106
if tasklist.size > 0
-
16
task_ids = '(' + tasklist.collect{|task| connection.quote(task.id)}.join(',') + ')'
-
8
connection.execute("update issues set
-
updated_on = #{connection.quote(self.updated_on)}, fixed_version_id = #{connection.quote(self.fixed_version_id)}, tracker_id = #{RbTask.tracker}
-
where id in #{task_ids}")
-
end
-
end
-
end
-
-
1
def assignable_releases
-
5
project.shared_releases
-
end
-
-
1
def release_id=(rid)
-
902
self.release = nil
-
902
write_attribute(:release_id, rid)
-
end
-
# def self.by_version(project)
-
# count_and_group_by(:project => project,
-
# :field => 'release_id',
-
# :joins => RbRelease.table_name)
-
# end
-
-
-
end
-
end
-
end
-
-
1
Issue.send(:include, Backlogs::IssuePatch) unless Issue.included_modules.include? Backlogs::IssuePatch
-
1
require_dependency 'user'
-
-
1
module Backlogs
-
1
module IssueStatusPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def backlog
-
535
return :success if is_closed? && (default_done_ratio.nil? || default_done_ratio == 100)
-
465
return :failure if is_closed?
-
465
return :new if is_default? || default_done_ratio == 0
-
124
return :in_progress
-
end
-
-
1
def backlog_is?(states)
-
475
states = [states] unless states.is_a?(Array)
-
475
raise "Not a valid state set #{states.inspect}" unless (states - [:success, :failure, :new, :in_progress]) == []
-
475
return states.include?(backlog)
-
end
-
end
-
end
-
end
-
-
1
IssueStatus.send(:include, Backlogs::IssueStatusPatch) unless IssueStatus.included_modules.include? Backlogs::IssueStatusPatch
-
1
require_dependency 'issues_controller'
-
1
require 'rubygems'
-
1
require 'nokogiri'
-
1
require 'json'
-
-
1
module Backlogs
-
1
module IssuesControllerPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
1
base.class_eval do
-
1
unloadable # Send unloadable so it will not be unloaded in development
-
1
after_filter :add_backlogs_fields, :only => [:index]
-
end
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def add_backlogs_fields
-
16
story_trackers = RbStory.trackers
-
-
16
case params[:format]
-
when 'xml'
-
body = Nokogiri::XML(response.body)
-
body.xpath('//issue').each{|issue|
-
next unless story_trackers.include?(Integer(issue.at('.//tracker')['id']))
-
issue << body.create_element('story_points', RbStory.find(issue.at('.//id').text).story_points.to_s)
-
}
-
response.body = body.to_xml
-
when 'json'
-
jsonp = (request.params[:callback] || request.params[:jsonp]).to_s.gsub(/[^a-zA-Z0-9_]/, '')
-
body = JSON.parse(jsonp.present? ? response.body.sub("#{jsonp}(","").chop : response.body)
-
body['issues'].each{|issue|
-
next unless story_trackers.include?(issue['tracker']['id'])
-
issue['story_points'] = RbStory.find(issue['id']).story_points
-
}
-
response.body = jsonp.present? ? "#{jsonp}(#{body.to_json})" : body.to_json
-
end
-
end
-
end
-
end
-
end
-
-
1
IssuesController.send(:include, Backlogs::IssuesControllerPatch) unless IssuesController.included_modules.include? Backlogs::IssuesControllerPatch
-
1
module Backlogs
-
1
class MergedArray
-
1
class FlexObject < Hash
-
1
def initialize(data = {})
-
60
super
-
-
60
data.each_pair {|k, v|
-
60
raise "#{k} is not a symbol" unless k.is_a?(Symbol)
-
60
self[k] = v
-
}
-
end
-
-
1
def [](key)
-
544
raise "Key '#{key}' does not exist" unless self.include?(key)
-
544
raise "Key '#{key}' is not a symbol" unless key.is_a?(Symbol)
-
544
return super(key)
-
end
-
-
1
def []=(key, value)
-
604
raise "Key '#{key}' is not a symbol" unless key.is_a?(Symbol)
-
604
super(key, value)
-
end
-
-
1
def method_missing(method_sym, *arguments, &block)
-
236
if method_sym.to_s =~ /=$/ && arguments.size == 1
-
40
self[method_sym.to_s.gsub(/=$/, '').intern] = arguments[0]
-
196
elsif self.include?(method_sym)
-
196
return self[method_sym]
-
else
-
super(method_sym, *arguments, &block)
-
end
-
end
-
-
1
def nilify
-
keys.each{|k| self[k] = nil}
-
end
-
end
-
-
1
def initialize(arrays = {})
-
20
@data = nil
-
20
merge(arrays)
-
end
-
-
1
def merge(arrays)
-
160
arrays.each_pair do |name, data|
-
140
raise "#{name} is not a symbol" unless name.is_a?(Symbol)
-
140
raise "#{name} is not a array" unless data.is_a?(Array)
-
-
140
if @data
-
120
raise "#{name} must have length of #{@data.size}, actual size #{data.size}" unless @data == [] || data.size == @data.size
-
480
@data.zip(data).each {|cell, v| cell[name] = v }
-
else
-
80
@data = data.collect{|v| FlexObject.new(name => v) }
-
end
-
end
-
end
-
-
1
def add(arrays)
-
16
return unless arrays
-
16
arrays.each_pair do |name, data|
-
48
next if data.nil?
-
48
raise "#{name} is not a symbol" unless name.is_a?(Symbol)
-
48
raise "#{name} is not a array" unless data.is_a?(Array)
-
48
raise "#{name} not initialized" unless @data && @data.size > 0 && @data[0].include?(name)
-
48
raise "data series '#{name}' is too long (got #{data.size}, maximum accepted #{@data.size})" if data.size > @data.size
-
-
48
data.each_with_index{|d, i|
-
144
@data[i][name] += d if d
-
}
-
end
-
end
-
-
1
def [](i)
-
return @data[i]
-
end
-
-
1
def each(&block)
-
64
@data.each {|cell| block.call(cell) }
-
end
-
1
def each_with_index(&block)
-
@data.each_with_index {|cell, index| block.call(cell, index) }
-
end
-
-
1
def collect(&block)
-
48
@data.collect {|cell| block.call(cell) }
-
end
-
-
1
def series(name)
-
272
@data.collect{|cell| cell[name]}
-
end
-
-
1
def to_s
-
return @data.to_s
-
end
-
1
def inspect
-
return @data.collect{|d| d.inspect}.join("\n")
-
end
-
end
-
end
-
1
require_dependency 'my_controller'
-
-
1
module Backlogs
-
1
module MyControllerPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
1
base.class_eval do
-
1
unloadable # Send unloadable so it will not be unloaded in development
-
1
after_filter :save_backlogs_preferences, :only => [:account]
-
end
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def save_backlogs_preferences
-
if request.post? && flash[:notice] == l(:notice_account_updated)
-
color = (params[:backlogs] ? params[:backlogs][:task_color] : '').to_s
-
if color == '' || color.match(/^#[A-Fa-f0-9]{6}$/)
-
User.current.backlogs_preference[:task_color] = color
-
else
-
flash[:notice] = "Invalid task color code #{color}"
-
end
-
end
-
end
-
end
-
end
-
end
-
-
1
MyController.send(:include, Backlogs::MyControllerPatch) unless MyController.included_modules.include? Backlogs::MyControllerPatch
-
1
require 'rubygems'
-
1
require 'prawn'
-
1
require 'prawn/measurement_extensions'
-
1
require 'net/http'
-
-
1
require 'yaml'
-
1
require 'uri/common'
-
1
require 'open-uri/cached'
-
1
require 'zlib'
-
1
require 'nokogiri'
-
-
1
unless defined?('ReliableTimout') || defined?(:ReliableTimout)
-
if Backlogs.gems.include?('system_timer')
-
require 'system_timer'
-
ReliableTimout = SystemTimer
-
else
-
require 'timeout'
-
ReliableTimout = Timeout
-
end
-
end
-
-
1
class String
-
1
def units_to_points
-
261
return Float(self) if self =~/[0-9]$/
-
-
261
m = self.match(/(.*)(mm|pt|in)$/)
-
261
if m
-
261
value = m[1].strip
-
261
units = m[2]
-
else
-
value = self
-
units = nil
-
end
-
261
raise "No units found for #{self.inspect}" unless m
-
-
261
value = Float(value.gsub(/\.$/, ''))
-
261
case units
-
when nil
-
return value
-
-
when 'mm'
-
261
return value * 2.8346457
-
-
when 'pt'
-
return value
-
-
when 'in'
-
return value * 72
-
-
else
-
raise "Unexpected unit specification for #{self}"
-
end
-
end
-
end
-
-
1
module BacklogsPrintableCards
-
1
class CardPageLayout
-
1
def initialize(layout)
-
@layout = layout
-
-
begin
-
@top_margin = layout['top_margin'].units_to_points
-
@height = layout['height'].units_to_points
-
@vertical_pitch = layout['vertical_pitch'].units_to_points
-
@vertical_pitch = @height if @vertical_pitch == 0
-
-
@left_margin = layout['left_margin'].units_to_points
-
@width = layout['width'].units_to_points
-
@horizontal_pitch = layout['horizontal_pitch'].units_to_points
-
@horizontal_pitch = @width if @horizontal_pitch == 0
-
-
@across = Integer(layout['across'])
-
@down = Integer(layout['down'])
-
-
@papersize = layout['papersize'].upcase
-
@name = layout['name']
-
@source = layout['source']
-
-
geom = Prawn::Document::PageGeometry::SIZES[@papersize]
-
if geom.nil?
-
Rails.logger.error "Backlogs printable cards: paper size '#{@papersize}' for label #{@name} not supported"
-
@valid = false
-
return
-
end
-
-
@paper_width = geom[0]
-
@paper_height = geom[1]
-
@paper_size = layout['papersize']
-
-
@valid = false
-
if @down < 1
-
Rails.logger.error "Backlogs printable cards: #{@name} has no rows"
-
elsif @across < 1
-
Rails.logger.error "Backlogs printable cards: #{@name} has no columns"
-
elsif @height > @vertical_pitch
-
Rails.logger.error "Backlogs printable cards: #{@name} card height exceeds vertical pitch"
-
elsif @width > @horizontal_pitch
-
Rails.logger.error "Backlogs printable cards: #{@name} card width exceeds horizontal pitch"
-
else
-
@valid = true
-
end
-
rescue => e
-
Rails.logger.error "Backlogs printable cards: error loading #{layout['name']}: #{e}"
-
Rails.logger.error(e.backtrace.join("\n"))
-
@valid = false
-
end
-
end
-
-
1
attr_reader :left_margin, :horizontal_pitch, :width
-
1
attr_reader :top_margin, :vertical_pitch, :height
-
1
attr_reader :across, :down
-
1
attr_reader :paper_width, :paper_height, :paper_size
-
1
attr_reader :source
-
1
attr_reader :valid
-
-
1
def self.selected
-
232
return @@layouts[Backlogs.setting[:card_spec]]
-
end
-
-
1
def self.available
-
1
return @@layouts.keys.sort
-
end
-
-
1
def to_yaml(opts={})
-
return @layout.reject{|k, v| k == 'name'}.to_yaml(opts)
-
end
-
-
1
def self.update
-
# clean up existing labels
-
malformed_labels = {}
-
-
['avery-iso-templates.xml', 'avery-other-templates.xml', 'avery-us-templates.xml', 'brother-other-templates.xml', 'dymo-other-templates.xml',
-
'maco-us-templates.xml', 'misc-iso-templates.xml', 'misc-other-templates.xml', 'misc-us-templates.xml', 'pearl-iso-templates.xml',
-
'uline-us-templates.xml', 'worldlabel-us-templates.xml', 'zweckform-iso-templates.xml'].each {|filename|
-
-
uri = URI.parse("http://git.gnome.org/browse/glabels/plain/templates/#{filename}")
-
labels = nil
-
-
if ! ENV['http_proxy'].blank?
-
begin
-
proxy = URI.parse(ENV['http_proxy'])
-
if proxy.userinfo
-
user, pass = proxy.userinfo.split(/:/)
-
else
-
user = pass = nil
-
end
-
labels = Net::HTTP::Proxy(proxy.host, proxy.port, user, pass).start(uri.host) {|http| http.get(uri.path)}.body
-
rescue URI::Error => e
-
puts "Setup proxy failed: #{e}"
-
labels = nil
-
end
-
end
-
-
begin
-
labels = Net::HTTP.get_response(uri).body if labels.nil?
-
rescue
-
labels = nil
-
end
-
-
if labels.nil?
-
puts "Could not fetch #{filename}"
-
next
-
end
-
-
doc = Nokogiri::XML(labels)
-
-
doc.xpath('Glabels-templates/Template').each { |specs|
-
label = nil
-
-
papersize = specs['size']
-
papersize = 'Letter' if papersize == 'US-Letter'
-
-
specs.xpath('Label-rectangle').each { |geom|
-
margin = nil
-
geom.xpath('Markup-margin').each { |m| margin = m['size'] }
-
margin = "1mm" if margin.blank?
-
-
geom.xpath('Layout').each { |layout|
-
label = {
-
'inner_margin' => margin,
-
'across' => Integer(layout['nx']),
-
'down' => Integer(layout['ny']),
-
'top_margin' => layout['y0'],
-
'height' => geom['height'],
-
'horizontal_pitch' => layout['dx'],
-
'left_margin' => layout['x0'],
-
'width' => geom['width'],
-
'vertical_pitch' => layout['dy'],
-
'papersize' => papersize,
-
'source' => 'glabel'
-
}
-
}
-
}
-
-
next if label.nil?
-
-
key = "#{specs['brand']} #{specs['part']}"
-
label['name'] = key
-
-
stock = CardPageLayout.new(label)
-
if !stock.valid
-
puts "Skipping malformed label '#{key}' from #{filename}"
-
malformed_labels[key] = stock.to_yaml
-
else
-
@@layouts[key] = stock if not @@layouts[key] or @@layouts[key].source == 'glabel'
-
-
specs.xpath('Alias').each { |also|
-
aliaskey = "#{also['brand']} #{also['part']}"
-
@@layouts[aliaskey] = stock if not @@layouts[aliaskey] or @@layouts[aliaskey].source == 'glabel'
-
}
-
end
-
}
-
}
-
-
File.open(File.dirname(__FILE__) + '/labels/labels.yaml', 'w') do |dump|
-
YAML.dump(@@layouts, dump)
-
end
-
File.open(File.dirname(__FILE__) + '/labels/labels-malformed.yaml', 'w') do |dump|
-
YAML.dump(malformed_labels, dump)
-
end
-
end
-
-
1
@@layouts ||= {}
-
1
begin
-
1
layouts = YAML::load_file(File.dirname(__FILE__) + '/labels/labels.yaml')
-
1
layouts.each_pair{|key, spec|
-
177
if spec.instance_of?(CardPageLayout)
-
177
layout = spec #new yaml stores and restores our class
-
else
-
layout = CardPageLayout.new(spec.merge({'name' => key})) #old layout.yaml might not have class information, so we get a hash
-
end
-
177
@@layouts[key] = layout if layout.valid
-
}
-
rescue => e
-
Rails.logger.error("Backlogs printable cards: problem loading labels: #{e}")
-
Rails.logger.error(e.backtrace.join("\n"))
-
end
-
end
-
-
# put the mixins in a separate class, seems to interfere with prawn otherwise
-
1
class Gravatar
-
1
case Backlogs.platform
-
when :redmine
-
1
include GravatarHelper::PublicMethods
-
when :chiliproject
-
include Gravatarify::Helper
-
end
-
1
include ERB::Util
-
-
1
def initialize(email, size)
-
# see conversion chart pt -> px @ http://sureshjain.wordpress.com/2007/07/06/53/
-
2
@url = gravatar_url(email, :size => (size * 16) / 12)
-
end
-
-
1
def image
-
2
begin
-
2
ReliableTimout.timeout(10) do
-
return open(@url)
-
end
-
rescue
-
2
return nil
-
end
-
end
-
-
1
attr_reader :url
-
end
-
-
1
class CardTemplate
-
1
def initialize(width, height, template)
-
4
@gravatar_online = true
-
-
4
f = nil
-
4
['-default', ''].each {|postfix|
-
8
t = File.dirname(__FILE__) + "/labels/#{template}#{postfix}.glabels"
-
8
f = t if File.exists?(t)
-
}
-
4
raise "No template for #{template}" unless f
-
4
label = Nokogiri::XML(Zlib::GzipReader.open(f))
-
-
4
bounds = label.xpath('//ns:Template/ns:Label-rectangle', 'ns' => 'http://snaught.com/glabels/2.2/')[0]
-
4
@template = { :x => bounds['width'].units_to_points, :y => bounds['height'].units_to_points}
-
-
4
@card = label.xpath('//ns:Objects', 'ns' => 'http://snaught.com/glabels/2.2/')[0]
-
4
@width = width
-
4
@height = height
-
end
-
-
1
def box(b, scaled=true)
-
return {
-
51
:x => (b['x'].units_to_points / @template[:x]) * @width,
-
51
:y => (1 - (b['y'].units_to_points / @template[:y])) * @height,
-
51
:w => (b['w'].units_to_points / @template[:x]) * @width,
-
51
:h => (b['h'].units_to_points / @template[:y]) * @height
-
51
}
-
end
-
-
1
def style(b)
-
49
s = b.xpath('ns:Span', 'ns' => 'http://snaught.com/glabels/2.2/')[0]
-
49
style = [s['font_weight'] == "Bold" ? 'bold' : nil, s['font_italic'] == "True" ? 'italic' : nil].compact.join('_')
-
49
style = 'normal' if style == ''
-
return {
-
:size => Integer(s['font_size']),
-
:style => style.intern
-
49
}
-
end
-
-
1
def line_width(obj)
-
7
return obj['line_width'].units_to_points
-
end
-
-
1
def color(obj, prop)
-
56
c = obj[prop]
-
56
return nil if c =~ /00$/
-
56
raise "Alpha channel not supported" unless c =~ /ff$/i
-
56
return c[2, 6]
-
end
-
-
1
def line(l)
-
return {
-
7
:x1 => (l['x'].units_to_points / @template[:x]) * @width,
-
7
:y1 => (1 - (l['y'].units_to_points / @template[:y])) * @height,
-
7
:x2 => ((l['x'].units_to_points + l['dx'].units_to_points) / @template[:x]) * @width,
-
7
:y2 => (1 - ((l['y'].units_to_points + l['dy'].units_to_points) / @template[:y])) * @height
-
7
}
-
end
-
-
1
def render(x, y, pdf, data)
-
7
default_stroke_color = pdf.stroke_color
-
7
default_fill_color = pdf.fill_color
-
-
7
pdf.bounding_box [x, y], :width => @width, :height => @height do
-
7
@card.children.each {|obj|
-
133
next if obj.text?
-
-
63
case obj.name
-
when 'Object-box'
-
dim = box(obj)
-
pdf.fill_color = color(obj, 'fill_color') || default_fill_color
-
pdf.stroke_color = color(obj, 'line_color') || default_stroke_color
-
pdf.line_width = line_width(obj)
-
-
pdf.stroke {
-
if color(obj, 'fill_color')
-
pdf.fill_rectangle [312,260], 180, 16
-
else
-
pdf.rectangle [dim[:x], dim[:y]], dim[:w], dim[:h]
-
end
-
}
-
-
-
when 'Object-line'
-
7
dim = line(obj)
-
7
pdf.line_width = line_width(obj)
-
7
pdf.stroke_color = color(obj, 'line_color') || default_stroke_color
-
-
7
pdf.stroke {
-
7
pdf.line([dim[:x1], dim[:y1]], [dim[:x2], dim[:y2]])
-
}
-
-
when 'Object-text'
-
49
dim = box(obj)
-
-
49
pdf.fill_color = color(obj.xpath('ns:Span', 'ns' => 'http://snaught.com/glabels/2.2/')[0], 'color') || default_fill_color
-
-
49
content = ''
-
49
obj.xpath('ns:Span', 'ns' => 'http://snaught.com/glabels/2.2/')[0].children.each {|t|
-
147
if t.text?
-
98
content << t.text
-
elsif t.name == 'Field'
-
49
f = data[t['name']]
-
49
raise "Unsupported card variable '#{t['name']}" unless f
-
49
content << f
-
else
-
raise "Unsupported text object '#{t.name}'"
-
end
-
}
-
-
49
content.strip!
-
-
49
s = style(obj)
-
49
pdf.font_size(s[:size]) do
-
49
Prawn::Text::Box.new(content, {:overflow => :ellipses, :at => [dim[:x], dim[:y]], :document => pdf, :width => dim[:w], :height => dim[:h], :style => s[:style]}).render
-
end
-
-
when 'Object-image'
-
7
if data['owner.email'] && @gravatar_online
-
2
dim = box(obj)
-
-
2
img = Gravatar.new(data['owner.email'], (dim[:h] < dim[:w]) ? dim[:h] : dim[:w]).image
-
2
if img
-
pdf.image img, :at => [dim[:x], dim[:y]], :width => dim[:w]
-
else
-
# if image loading fails once, stop loading images for this rendering
-
2
@gravatar_online = false
-
end
-
end
-
-
else
-
raise "Unsupported object '#{obj.name}'"
-
end
-
}
-
end
-
-
7
pdf.stroke_color = default_stroke_color
-
7
pdf.fill_color = default_fill_color
-
end
-
end
-
-
1
class PrintableCards
-
1
include Redmine::I18n
-
-
1
def initialize(stories, with_tasks, lang)
-
2
set_language_if_valid lang
-
-
2
@label = CardPageLayout.selected
-
2
@pdf = Prawn::Document.new(
-
:page_layout => :portrait,
-
:left_margin => 0,
-
:right_margin => 0,
-
:top_margin => 0,
-
:bottom_margin => 0,
-
:page_size => @label ? @label.paper_size : 'A4')
-
-
2
if !@label
-
@pdf.text("No (valid) label layout was selected. Your rails log will probably have more details on the exact problem.")
-
else
-
2
@story = CardTemplate.new(@label.width, @label.height, 'story')
-
2
@task = CardTemplate.new(@label.width, @label.height, 'task')
-
-
2
fontdir = File.dirname(__FILE__) + '/ttf'
-
2
@pdf.font_families.update(
-
"DejaVuSans" => {
-
:bold => "#{fontdir}/DejaVuSans-Bold.ttf",
-
:italic => "#{fontdir}/DejaVuSans-Oblique.ttf",
-
:bold_italic => "#{fontdir}/DejaVuSans-BoldOblique.ttf",
-
:normal => "#{fontdir}/DejaVuSans.ttf"
-
}
-
)
-
2
@pdf.font "DejaVuSans"
-
-
2
@cards = 0
-
-
2
case Backlogs.setting[:taskboard_card_order]
-
when 'tasks_follow_story'
-
stories.each { |story|
-
add(story)
-
-
if with_tasks
-
story.descendants.each {|task|
-
add(task)
-
}
-
end
-
}
-
-
when 'stories_then_tasks'
-
stories.each { |story|
-
add(story)
-
}
-
-
if with_tasks
-
@cards = 0
-
@pdf.start_new_page
-
-
stories.each { |story|
-
story.descendants.each {|task|
-
add(task)
-
}
-
}
-
end
-
-
else # 'story_follows_tasks'
-
2
stories.each { |story|
-
7
if with_tasks
-
3
story.descendants.each {|task|
-
add(task)
-
}
-
end
-
-
7
add(story)
-
}
-
end
-
end
-
end
-
-
1
attr_reader :pdf
-
-
1
def add(issue)
-
7
row = @cards % @label.down
-
7
col = Integer(@cards / @label.down) % @label.across
-
7
@cards += 1
-
-
7
@pdf.start_new_page if row == 0 and col == 0 and @cards != 1
-
-
7
x = @label.left_margin + (@label.horizontal_pitch * col)
-
7
y = @label.paper_height - (@label.top_margin + (@label.vertical_pitch * row))
-
-
7
data = {}
-
7
if issue.is_task?
-
data['story.position'] = issue.story.position ? issue.story.position : l(:label_not_prioritized)
-
data['story.id'] = issue.story.id
-
data['story.subject'] = issue.story.subject
-
-
data['id'] = issue.id
-
data['subject'] = issue.subject.to_s.strip
-
data['description'] = issue.description.to_s.strip; data['description'] = data['subject'] if data['description'] == ''
-
data['category'] = issue.category ? issue.category.name : ''
-
data['hours.estimated'] = (issue.estimated_hours || '?').to_s + ' ' + l(:label_hours)
-
data['position'] = issue.position ? issue.position : l(:label_not_prioritized)
-
data['path'] = (issue.self_and_ancestors.reverse.collect{|i| "#{i.tracker.name} ##{i.id}"}.join(" : ")) + " (#{data['story.position']})"
-
data['sprint.name'] = issue.fixed_version ? issue.fixed_version.name : I18n.t(:backlogs_product_backlog)
-
data['owner'] = issue.assigned_to.blank? ? "" : "#{issue.assigned_to.name}"
-
data['owner.email'] = issue.assigned_to.blank? ? nil : issue.assigned_to.mail.to_s.downcase
-
-
card = @task
-
-
elsif issue.is_story?
-
7
data['id'] = issue.id
-
7
data['subject'] = issue.subject
-
7
data['description'] = issue.description.to_s.strip; data['description'] = data['subject'] if data['description'] == ''
-
7
data['category'] = issue.category ? issue.category.name : ''
-
7
data['size'] = (issue.story_points ? "#{issue.story_points}" : '?') + ' ' + l(:label_points)
-
7
data['position'] = issue.position ? issue.position : l(:label_not_prioritized)
-
14
data['path'] = (issue.self_and_ancestors.reverse.collect{|i| "#{i.tracker.name} ##{i.id}"}.join(" : ")) + " (#{data['position']})"
-
7
data['sprint.name'] = issue.fixed_version ? issue.fixed_version.name : I18n.t(:backlogs_product_backlog)
-
7
data['owner'] = issue.assigned_to.blank? ? "" : "#{issue.assigned_to.name}"
-
7
data['owner.email'] = issue.assigned_to.blank? ? nil : issue.assigned_to.mail.to_s.downcase
-
-
7
card = @story
-
-
else
-
raise "Unsupported card type '#{type}'"
-
-
end
-
-
77
data.keys.each {|d| data[d] = data[d].to_s }
-
-
7
card.render(x, y, @pdf, data)
-
end
-
-
end
-
end
-
1
require_dependency 'project'
-
-
1
module Backlogs
-
1
class Statistics
-
1
def initialize(project)
-
2
@project = project
-
2
@statistics = {:succeeded => [], :failed => [], :values => {}}
-
-
2
@active_sprint = @project.active_sprint
-
2
@past_sprints = RbSprint.find(:all,
-
:conditions => ["project_id = ? and not(effective_date is null or sprint_start_date is null) and effective_date < ?", @project.id, Date.today],
-
:order => "effective_date desc",
-
:limit => 5).select(&:has_burndown?)
-
2
@all_sprints = (@past_sprints + [@active_sprint]).compact
-
-
6
@all_sprints.each{|sprint| sprint.burndown.direction = :up }
-
6
days = @past_sprints.collect{|s| s.days.size}.sum
-
2
if days != 0
-
5
@points_per_day = @past_sprints.collect{|s| s.burndown.data[:points_committed][0]}.compact.sum / days
-
end
-
-
2
if @all_sprints.size != 0
-
5
@velocity = @past_sprints.collect{|sprint| sprint.burndown.data[:points_accepted][-1].to_f}
-
1
@velocity_stddev = stddev(@velocity)
-
end
-
-
6
spent_hours = @past_sprints.collect{|sprint| sprint.spent_hours}
-
2
@spent_hours_per_point = spent_hours.sum / @velocity.sum unless spent_hours.nil? || @velocity.nil? || @velocity.sum == 0
-
-
2
@product_backlog = RbStory.product_backlog(@project, 10)
-
-
2
hours_per_point = []
-
2
@all_sprints.each {|sprint|
-
4
hours = sprint.burndown.data[:hours_remaining][0].to_f
-
4
next if hours == 0.0
-
hours_per_point << sprint.burndown.data[:points_committed][0].to_f / hours
-
}
-
2
@hours_per_point_stddev = stddev(hours_per_point)
-
2
@hours_per_point = hours_per_point.sum.to_f / hours_per_point.size unless hours_per_point.size == 0
-
-
2
Statistics.active_tests.sort.each{|m|
-
18
r = send(m.intern)
-
18
next if r.nil? # this test deems itself irrelevant
-
@statistics[r ? :succeeded : :failed] <<
-
15
(m.to_s.gsub(/^test_/, '') + (r ? '' : '_failed'))
-
}
-
2
Statistics.stats.sort.each{|m|
-
12
v = send(m.intern)
-
12
@statistics[:values][m.to_s.gsub(/^stat_/, '')] = v unless v.nil? || (v.respond_to?(:"nan?") && v.nan?) || (v.respond_to?(:"infinite?") && v.infinite?)
-
}
-
-
2
if @statistics[:succeeded].size == 0 && @statistics[:failed].size == 0
-
@score = 100 # ?
-
else
-
2
@score = (@statistics[:succeeded].size * 100) / (@statistics[:succeeded].size + @statistics[:failed].size)
-
end
-
end
-
-
1
attr_reader :statistics, :score
-
1
attr_reader :active_sprint, :past_sprints
-
1
attr_reader :hours_per_point
-
1
attr_reader :spent_hours_per_point
-
-
1
def stddev(values)
-
3
median = values.sum / values.size.to_f
-
7
variance = 1.0 / (values.size * values.inject(0){|acc, v| acc + (v-median)**2})
-
3
return Math.sqrt(variance)
-
end
-
-
1
def self.available
-
return Statistics.instance_methods.select{|m| m =~ /^test_/}.collect{|m| m.split('_', 2).collect{|s| s.intern}}
-
end
-
-
1
def self.active_tests
-
# test this!
-
418
return Statistics.instance_methods.select{|m| m =~ /^test_/}.reject{|m| Backlogs.setting["disable_stats_#{m}".intern] }
-
end
-
-
1
def self.active
-
return Statistics.active_tests.collect{|m| m.split('_', 2).collect{|s| s.intern}}
-
end
-
-
1
def self.stats
-
400
return Statistics.instance_methods.select{|m| m =~ /^stat_/}
-
end
-
-
1
def info_no_active_sprint
-
return !@active_sprint
-
end
-
-
1
def test_product_backlog_filled
-
2
return (@project.status != Project::STATUS_ACTIVE || @product_backlog.length != 0)
-
end
-
-
1
def test_product_backlog_sized
-
4
return !@product_backlog.detect{|s| s.story_points.blank? }
-
end
-
-
1
def test_sprints_sized
-
6
return !Issue.exists?(["story_points is null and fixed_version_id in (?) and tracker_id in (?)", @all_sprints.collect{|s| s.id}, RbStory.trackers])
-
end
-
-
1
def test_sprints_estimated
-
6
return !Issue.exists?(["estimated_hours is null and fixed_version_id in (?) and tracker_id = ?", @all_sprints.collect{|s| s.id}, RbTask.tracker])
-
end
-
-
1
def test_sprint_notes_available
-
3
return !@past_sprints.detect{|s| !s.has_wiki_page}
-
end
-
-
1
def test_active
-
2
return (@project.status != Project::STATUS_ACTIVE || (@active_sprint && @active_sprint.activity))
-
end
-
-
1
def test_yield
-
2
accepted = []
-
2
@past_sprints.each {|sprint|
-
4
bd = sprint.burndown
-
4
bd.direction = :up
-
4
c = bd.data[:points_committed][-1]
-
4
a = bd.data[:points_accepted][-1]
-
4
next unless c && a && c != 0
-
-
accepted << [(a * 100.0) / c, 100.0].min
-
}
-
2
return false if accepted == []
-
return (stddev(accepted) < 10) # magic number
-
end
-
-
1
def test_committed_velocity_stable
-
2
return (@velocity_stddev && @velocity_stddev < 4) # magic number!
-
end
-
-
1
def test_sizing_consistent
-
2
return (@hours_per_point_stddev < 4) # magic number
-
end
-
-
1
def stat_sprints
-
2
return @past_sprints.size
-
end
-
-
1
def stat_velocity
-
2
return nil unless @velocity && @velocity.size > 0
-
1
return @velocity.sum / @velocity.size
-
end
-
-
1
def stat_velocity_stddev
-
2
return @velocity_stddev unless @velocity_stddev.is_a? Float
-
1
return '%.2f' % @velocity_stddev
-
end
-
-
1
def stat_sizing_stddev
-
2
return @hours_per_point_stddev unless @hours_per_point_stddev.is_a? Float
-
2
return '%.2f' % @hours_per_point_stddev
-
end
-
-
1
def stat_hours_per_point
-
2
return @hours_per_point unless @hours_per_point.is_a? Float
-
return '%.2f' % @hours_per_point
-
end
-
-
1
def stat_spent_hours_per_point
-
2
return nil unless @spent_hours_per_point
-
return '%.2f' % @spent_hours_per_point
-
end
-
end
-
-
1
module ProjectPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
1
base.class_eval do
-
1
unloadable
-
1
has_many :releases, :class_name => 'RbRelease', :inverse_of => :project, :dependent => :destroy, :order => "#{RbRelease.table_name}.release_start_date DESC, #{RbRelease.table_name}.name DESC"
-
1
include Backlogs::ActiveRecord::Attributes
-
end
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
-
1
def scrum_statistics
-
## pretty expensive to compute, so if we're calling this multiple times, return the cached results
-
2
@scrum_statistics ||= Backlogs::Statistics.new(self)
-
end
-
-
1
def rb_project_settings
-
83
project_settings = RbProjectSettings.first(:conditions => ["project_id = ?", self.id])
-
83
unless project_settings
-
21
project_settings = RbProjectSettings.new( :project_id => self.id)
-
21
project_settings.save
-
end
-
83
project_settings
-
end
-
-
1
def projects_in_shared_product_backlog
-
#sharing off: only the product itself is in the product backlog
-
#sharing on: subtree is included in the product backlog
-
154
if Backlogs.setting[:sharing_enabled] and self.rb_project_settings.show_stories_from_subprojects
-
58
self.self_and_descendants.visible.active
-
else
-
96
[self]
-
end
-
#TODO have an explicit association map which project shares its issues into other product backlogs
-
end
-
-
#return sprints which are
-
# 1. open in project,
-
# 2. share to project,
-
# 3. share to project but are scoped to project and subprojects
-
#depending on sharing mode
-
1
def open_shared_sprints
-
116
if Backlogs.setting[:sharing_enabled]
-
60
order = Backlogs.setting[:sprint_sort_order] == 'desc' ? 'DESC' : 'ASC'
-
199
shared_versions.visible.scoped(:conditions => {:status => ['open', 'locked']}, :order => "sprint_start_date #{order}, effective_date #{order}").collect{|v| v.becomes(RbSprint) }
-
else #no backlog sharing
-
56
RbSprint.open_sprints(self)
-
end
-
end
-
-
#depending on sharing mode
-
1
def closed_shared_sprints
-
1
if Backlogs.setting[:sharing_enabled]
-
1
order = Backlogs.setting[:sprint_sort_order] == 'desc' ? 'DESC' : 'ASC'
-
3
shared_versions.visible.scoped(:conditions => {:status => ['closed']}, :order => "sprint_start_date #{order}, effective_date #{order}").collect{|v| v.becomes(RbSprint) }
-
else #no backlog sharing
-
RbSprint.closed_sprints(self)
-
end
-
end
-
-
1
def active_sprint
-
@active_sprint ||= RbSprint.find(:first, :conditions => [
-
"project_id = ? and status = 'open' and not (sprint_start_date is null or effective_date is null) and ? between sprint_start_date and effective_date",
-
125
self.id, (Time.zone ? Time.zone : Time).now.beginning_of_day
-
125
])
-
end
-
-
1
def open_releases_by_date
-
61
order = Backlogs.setting[:sprint_sort_order] == 'desc' ? 'DESC' : 'ASC'
-
61
(Backlogs.setting[:sharing_enabled] ? shared_releases : releases).
-
visible.open.
-
order("#{RbRelease.table_name}.release_start_date #{order}, #{RbRelease.table_name}.release_end_date #{order}")
-
end
-
-
1
def shared_releases
-
92
if new_record?
-
RbRelease.scoped(:include => :project,
-
:conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{RbRelease.table_name}.sharing = 'system'")
-
else
-
@shared_releases ||= begin
-
92
r = root? ? self : root
-
92
RbRelease.scoped(:include => :project,
-
:conditions => "#{Project.table_name}.id = #{id}" +
-
" OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
-
" #{RbRelease.table_name}.sharing = 'system'" +
-
" OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{RbRelease.table_name}.sharing = 'tree')" +
-
" OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{RbRelease.table_name}.sharing IN ('hierarchy', 'descendants'))" +
-
" OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{RbRelease.table_name}.sharing = 'hierarchy')" +
-
"))")
-
92
end
-
end
-
end
-
-
end
-
end
-
end
-
-
1
Project.send(:include, Backlogs::ProjectPatch) unless Project.included_modules.include? Backlogs::ProjectPatch
-
1
require_dependency 'projects_helper'
-
-
1
module Backlogs
-
1
module ProjectsHelperPatch
-
-
1
def self.included(base)
-
1
base.send(:include, InstanceMethods)
-
1
base.class_eval do
-
1
unloadable
-
1
alias_method_chain :project_settings_tabs, :backlogs
-
end
-
end
-
-
1
module InstanceMethods
-
-
1
def project_settings_tabs_with_backlogs
-
7
tabs = project_settings_tabs_without_backlogs
-
tabs << {:name => 'backlogs',
-
:action => :manage_project_backlogs,
-
:partial => 'backlogs/project_settings',
-
:label => :label_backlogs
-
} if @project.module_enabled?('backlogs') and
-
7
User.current.allowed_to?(:configure_backlogs, nil, :global=>true)
-
7
return tabs
-
end
-
-
end
-
-
end
-
end
-
-
1
ProjectsHelper.send(:include, Backlogs::ProjectsHelperPatch) unless ProjectsHelper.included_modules.include? Backlogs::ProjectsHelperPatch
-
-
1
require_dependency 'query'
-
1
require 'erb'
-
-
1
module Backlogs
-
1
class RbERB
-
1
def initialize(s)
-
@sql = ERB.new(s)
-
end
-
-
1
def to_s
-
return @sql.result
-
end
-
end
-
-
1
module QueryPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
# Same as typing in the class
-
1
base.class_eval do
-
1
unloadable # Send unloadable so it will not be unloaded in development
-
1
base.add_available_column(QueryColumn.new(:story_points, :sortable => "#{Issue.table_name}.story_points"))
-
1
base.add_available_column(QueryColumn.new(:velocity_based_estimate))
-
1
base.add_available_column(QueryColumn.new(:position, :sortable => "#{Issue.table_name}.position"))
-
1
base.add_available_column(QueryColumn.new(:remaining_hours, :sortable => "#{Issue.table_name}.remaining_hours"))
-
1
base.add_available_column(QueryColumn.new(:release, :sortable => "#{RbRelease.table_name}.name", :groupable => true))
-
-
1
alias_method_chain :available_filters, :backlogs_issue_type
-
1
alias_method_chain :sql_for_field, :backlogs_issue_type
-
end
-
end
-
-
1
module InstanceMethods
-
1
def available_filters_with_backlogs_issue_type
-
414
@available_filters = available_filters_without_backlogs_issue_type
-
-
414
if RbStory.trackers.length == 0 or RbTask.tracker.blank?
-
backlogs_filters = { }
-
else
-
414
backlogs_filters = {
-
# mother of *&@&^*@^*#.... order "20" is a magical constant in RM2.2 which means "I'm a custom field". What. The. Fuck.
-
"backlogs_issue_type" => { :type => :list,
-
:name => l(:field_backlogs_issue_type),
-
:values => [[l(:backlogs_story), "story"], [l(:backlogs_task), "task"], [l(:backlogs_impediment), "impediment"], [l(:backlogs_any), "any"]],
-
:order => 21 },
-
"story_points" => { :type => :float,
-
:name => l(:field_story_points),
-
:order => 22 }
-
}
-
end
-
-
414
if project
-
414
backlogs_filters["release_id"] = {
-
:type => :list_optional,
-
:name => l(:field_release),
-
:values => RbRelease.find(:all, :conditions => ["project_id IN (?)", project], :order => 'name ASC').collect { |d| [d.name, d.id.to_s]},
-
:order => 21
-
}
-
end
-
414
@available_filters = @available_filters.merge(backlogs_filters)
-
end
-
-
1
def sql_for_field_with_backlogs_issue_type(field, operator, value, db_table, db_field, is_custom_filter=false)
-
72
return sql_for_field_without_backlogs_issue_type(field, operator, value, db_table, db_field, is_custom_filter) unless field == "backlogs_issue_type"
-
-
20
db_table = Issue.table_name
-
-
20
sql = []
-
-
20
selected_values = values_for(field)
-
20
selected_values = ['story', 'task'] if selected_values.include?('any')
-
-
20
story_trackers = RbStory.trackers(:type=>:string)
-
60
all_trackers = (RbStory.trackers + [RbTask.tracker]).collect{|val| "#{val}"}.join(",")
-
-
20
selected_values.each { |val|
-
32
case val
-
when "story"
-
18
sql << "(#{db_table}.tracker_id in (#{story_trackers}))"
-
-
when "task"
-
12
sql << "(#{db_table}.tracker_id = #{RbTask.tracker})"
-
-
when "impediment"
-
sql << "(#{db_table}.id in (
-
select issue_from_id
-
from issue_relations ir
-
join issues blocked on
-
blocked.id = ir.issue_to_id
-
and blocked.tracker_id in (#{all_trackers})
-
where ir.relation_type = 'blocks'
-
2
))"
-
end
-
}
-
-
20
case operator
-
when "="
-
20
sql = sql.join(" or ")
-
-
when "!"
-
sql = "not (" + sql.join(" or ") + ")"
-
end
-
-
20
return sql
-
end
-
end
-
-
1
module ClassMethods
-
# Setter for +available_columns+ that isn't provided by the core.
-
1
def available_columns=(v)
-
self.available_columns = (v)
-
end
-
-
# Method to add a column to the +available_columns+ that isn't provided by the core.
-
1
def add_available_column(column)
-
self.available_columns << (column)
-
end
-
end
-
end
-
end
-
-
1
Query.send(:include, Backlogs::QueryPatch) unless Query.included_modules.include? Backlogs::QueryPatch
-
1
require 'rubygems'
-
1
require 'yaml'
-
1
require 'singleton'
-
-
1
unless defined?('ReliableTimout') || defined?(:ReliableTimout)
-
if Backlogs.gems.include?('system_timer')
-
require 'system_timer'
-
ReliableTimout = SystemTimer
-
else
-
require 'timeout'
-
ReliableTimout = Timeout
-
end
-
end
-
-
1
module Backlogs
-
1
def version
-
1
root = File.expand_path('..', File.dirname(__FILE__))
-
1
git = File.join(root, '.git')
-
1
v = Redmine::Plugin.find(:redmine_backlogs).version
-
-
1
g = nil
-
1
if File.directory?(git)
-
1
Dir.chdir(root)
-
1
g = `git describe --tags --abbrev=10`
-
1
g = "(#{g.strip})" if g
-
end
-
-
1
v = [v, g].compact.join(' ')
-
1
v = '?' if v == ''
-
1
return v
-
end
-
1
module_function :version
-
-
1
def development?
-
return !Rails.env.production?
-
end
-
1
module_function :"development?"
-
-
1
def platform_support(raise_error = false)
-
4199
travis = nil # needed so versions isn't block-scoped in the timeout
-
4199
begin
-
4199
ReliableTimout.timeout(10) { travis = YAML::load(open('https://raw.github.com/backlogs/redmine_backlogs/master/.travis.yml').read) }
-
rescue
-
4199
travis = YAML::load(File.open(File.join(File.dirname(__FILE__), '..', '.travis.yml')).read)
-
end
-
-
4199
matrix = []
-
4199
travis['rvm'].each{|rvm|
-
8398
travis['env'].each{|env|
-
83980
matrix << {'ruby' => rvm, 'env' => env}
-
}
-
}
-
-
4199
travis['matrix']['exclude'].each{|exc|
-
# if all values of the exclusion match, remove the cell
-
2049112
matrix.delete_if{|cell| exc.keys.collect{|k| cell[k] == exc[k] ? '' : 'x'}.join('') == '' }
-
}
-
4199
travis['matrix']['allow_failures'].each{|af|
-
# if all values of the allowed failure match, the cell is unsupported
-
8398
matrix.each{|cell|
-
335920
cell[:unsupported] = true if af.keys.collect{|k| cell[k] == af[k] ? '' : 'x'}.join('') == ''
-
}
-
}
-
4199
matrix.each{|cell|
-
83980
cell[:version] = cell.delete('env').gsub(/^REDMINE_VER=/, '').gsub(/\s.*/, '')
-
83980
cell[:platform] = (cell[:version] =~ /^[0-9]/ ? :redmine : :chiliproject)
-
}
-
-
4199
plugin_version = Redmine::Plugin.find(:redmine_backlogs).version
-
4199
return "#{Redmine::VERSION}. You are running backlogs #{plugin_version}, latest version is #{travis['release']}" if plugin_version != travis['release']
-
-
supported = matrix.select{|cell| cell[:platform] == platform}
-
raise "Unsupported platform #{platform}" unless supported.size > 0
-
-
platform_version = Redmine::VERSION.to_a.collect{|d| d.to_s}
-
ruby_version = RUBY_VERSION.split('.')
-
supported.each{|cell|
-
v = cell[:version].split('.')
-
next unless platform_version[0,v.length] == v
-
-
v = cell[:ruby].split('.')
-
next unless ruby_version[0,v.length] == v
-
-
return "#{Redmine::VERSION}#{cell[:unsupported] ? '(unsupported but might work)' : ''}"
-
}
-
-
return "#{Redmine::VERSION} (DEVELOPMENT MODE)" if development?
-
-
msg = "#{Redmine::VERSION} on #{RUBY_VERSION} (NOT SUPPORTED; please install #{platform} #{supported.reject{|v| v[:unsupported]}.collect{|v| "#{v[:version]} on #{v[:ruby]}"}.uniq.sort.join(' / ')}"
-
raise msg if raise_error
-
return msg
-
end
-
1
module_function :platform_support
-
-
1
def os
-
4199
return :windows if RUBY_PLATFORM =~ /cygwin|windows|mswin|mingw|bccwin|wince|emx/
-
4199
return :unix if RUBY_PLATFORM =~ /darwin|linux/
-
return :java if RUBY_PLATFORM =~ /java/
-
return nil
-
end
-
1
module_function :os
-
-
1
def gems
-
33592
installed = Hash[*(['json', 'system_timer', 'nokogiri', 'open-uri/cached', 'holidays', 'icalendar', 'prawn'].collect{|gem| [gem, false]}.flatten)]
-
4199
installed.delete('system_timer') unless os == :unix && RUBY_VERSION =~ /^1\.8\./
-
4199
installed.keys.each{|gem|
-
25194
begin
-
25194
require gem
-
25194
installed[gem] = true
-
rescue LoadError
-
end
-
}
-
4199
return installed
-
end
-
1
module_function :gems
-
-
1
def trackers
-
4198
return {:task => !!Tracker.find_by_id(RbTask.tracker), :story => !RbStory.trackers.empty?, :default_priority => !IssuePriority.default.nil?}
-
end
-
1
module_function :trackers
-
-
1
def task_workflow(project)
-
14
return false unless RbTask.tracker
-
-
14
roles = User.current.roles_for_project(@project)
-
14
tracker = Tracker.find(RbTask.tracker)
-
-
14
[false, true].each{|creator|
-
28
[false, true].each{|assignee|
-
56
tracker.issue_statuses.each {|status|
-
336
status.new_statuses_allowed_to(roles, tracker, creator, assignee).each{|s|
-
return true
-
}
-
}
-
}
-
}
-
end
-
1
module_function :task_workflow
-
-
1
def migrated?
-
83960
available = Dir[File.join(File.dirname(__FILE__), '../db/migrate/*.rb')].collect{|m| Integer(File.basename(m).split('_')[0].gsub(/^0+/, ''))}.sort
-
4198
return true if available.size == 0
-
4198
available = available[-1]
-
-
4198
ran = []
-
4198
Setting.connection.execute("select version from schema_migrations where version like '%-redmine_backlogs'").each{|m|
-
79762
ran << Integer((m.is_a?(Hash) ? m.values : m)[0].split('-')[0])
-
}
-
4198
return false if ran.size == 0
-
4198
ran = ran.sort[-1]
-
-
4198
return ran >= available
-
end
-
1
module_function :migrated?
-
-
1
def configured?(project=nil)
-
29386
return false if Backlogs.gems.values.reject{|installed| installed}.size > 0
-
16792
return false if Backlogs.trackers.values.reject{|configured| configured}.size > 0
-
4198
return false unless Backlogs.migrated?
-
4198
return false unless project.nil? || project.enabled_module_names.include?("backlogs")
-
4198
begin
-
4198
platform_support(true)
-
rescue
-
return false
-
end
-
-
4198
return true
-
end
-
1
module_function :configured?
-
-
1
def platform
-
2
unless @platform
-
1
begin
-
1
ChiliProject::VERSION
-
@platform = :chiliproject
-
rescue NameError
-
1
@platform = :redmine
-
end
-
end
-
2
return @platform
-
end
-
1
module_function :platform
-
-
1
class SettingsProxy
-
1
include Singleton
-
-
1
def [](key)
-
226069
key = key.intern if key.is_a?(String)
-
226069
settings = safe_load
-
# add alternate loading because settings loading on ruby 1.9.3 seems to sometimes convert keys to strings on save.
-
226069
return settings[key] || settings[key.to_s]
-
end
-
-
1
def []=(key, value)
-
978
key = key.intern if key.is_a?(String)
-
978
settings = safe_load
-
978
settings[key] = value
-
978
Setting.plugin_redmine_backlogs = settings
-
end
-
-
1
def to_h
-
893
h = safe_load
-
893
h.freeze
-
893
h
-
end
-
-
1
private
-
-
1
def safe_load
-
# At the first migration, the settings table will not exist
-
227940
return {} unless Setting.table_exists?
-
-
227940
settings = Setting.plugin_redmine_backlogs.dup
-
227940
if settings.is_a?(String)
-
Rails.logger.error "Unable to load settings"
-
return {}
-
end
-
227940
settings
-
end
-
end
-
-
1
def setting
-
227047
SettingsProxy.instance
-
end
-
1
module_function :setting
-
1
def settings
-
893
SettingsProxy.instance.to_h
-
end
-
1
module_function :settings
-
end
-
1
require_dependency 'user'
-
-
1
module Backlogs
-
1
module TrackerPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def backlog?
-
35
return (issue_statuses.collect{|s| s.backlog}.compact.uniq.size == 4)
-
end
-
-
1
def status_for_done_ratio(r)
-
return (issue_statuses.select{|s| !s.default_done_ratio.nil? && s.default_done_ratio < r && !s.is_closed?}.sort{|a, b| b.default_done_ratio <=> a.default_done_ratio} + [nil])[0]
-
end
-
end
-
end
-
end
-
-
1
Tracker.send(:include, Backlogs::TrackerPatch) unless Tracker.included_modules.include? Backlogs::TrackerPatch
-
1
require_dependency 'user'
-
-
1
module Backlogs
-
1
class Preference
-
1
def initialize(user)
-
114
@user = user
-
114
@prefs = {}
-
end
-
-
1
def []=(attr, value)
-
76
prefixed = "backlogs_#{attr}".intern
-
-
76
case attr
-
when :task_color
-
76
value = value.to_s.strip
-
76
value = "##{value}" if value =~ /^[0-9A-F]{6}$/i
-
76
raise "Color format must be 6 hex digit string or empty, supplied value: #{value.inspect}" unless value == '' || value =~ /^#[0-9A-F]{6}$/i
-
76
value.upcase!
-
else
-
raise "Unsupported attribute '#{attr}'"
-
end
-
-
76
@user.pref[prefixed] = value
-
76
@prefs[prefixed] = value
-
76
@user.pref.save!
-
end
-
-
1
def [](attr)
-
342
prefixed = "backlogs_#{attr}".intern
-
-
342
unless @prefs.include?(prefixed)
-
228
value = @user.pref[prefixed].to_s.strip
-
-
228
case attr
-
when :task_color
-
114
if value == '' # assign default
-
532
colors = UserPreference.find(:all).collect{|p| p[prefixed].to_s.upcase}.select{|p| p != ''}
-
76
min = 0x999999
-
76
50.times do
-
76
candidate = "##{(min + rand(0xFFFFFF-min)).to_s(16).upcase}"
-
76
next if colors.include?(candidate)
-
76
value = candidate
-
76
break
-
end
-
76
self[attr] = value
-
end
-
-
when :task_color_light
-
114
value = self[:task_color].to_s
-
114
value = Backlogs::Color.new(value).lighten(0.5) unless value == ''
-
-
else
-
raise "Unsupported attribute '#{attr}'"
-
end
-
-
228
@prefs[prefixed] = value
-
end
-
-
342
return @prefs[prefixed]
-
end
-
end
-
-
1
module UserPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
-
1
def backlogs_preference
-
228
@backlogs_preference ||= Backlogs::Preference.new(self)
-
end
-
-
end
-
end
-
end
-
-
1
User.send(:include, Backlogs::UserPatch) unless User.included_modules.include? Backlogs::UserPatch
-
1
require_dependency 'version'
-
-
1
module Backlogs
-
1
module VersionPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
1
base.class_eval do
-
1
unloadable
-
-
1
has_one :sprint_burndown, :class_name => RbSprintBurndown, :dependent => :destroy
-
-
1
after_save :clear_burndown
-
-
1
include Backlogs::ActiveRecord::Attributes
-
end
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def clear_burndown
-
402
self.burndown.touch!
-
end
-
-
# load on demand
-
1
def burndown
-
2363
self.sprint_burndown = self.create_sprint_burndown(:version_id => self.id) unless self.new_record? || self.sprint_burndown
-
2363
return self.sprint_burndown
-
end
-
-
1
def days
-
#return Day objects. Version stores start and effective date without timezone. These are used to filter history entries and thus the zone of these days are those of the history dates
-
7891
return nil unless self.sprint_start_date && self.effective_date
-
198725
(self.sprint_start_date - 1 .. self.effective_date).to_a.select{|d| Backlogs.setting[:include_sat_and_sun] || ![0,6].include?(d.wday)}
-
end
-
1
def has_burndown?
-
3914
return (self.days || []).size != 0
-
end
-
-
end
-
end
-
end
-
-
1
Version.send(:include, Backlogs::VersionPatch) unless Version.included_modules.include? Backlogs::VersionPatch
-
# Copyright (c) 2007 McClain Looney
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
-
-
# Implements a color (r,g,b + a) with conversion to/from web format (eg #aabbcc), and
-
# with a number of utilities to lighten, darken and blend values.
-
1
module Backlogs
-
1
class Color
-
-
1
attr_reader :r, :g, :b, :a
-
-
# Table for conversion to hex
-
1
HEXVAL = (('0'..'9').to_a).concat(('A'..'F').to_a).freeze
-
# Default value for #darken, #lighten etc.
-
1
BRIGHTNESS_DEFAULT = 0.2
-
-
# Constructor. Inits to white (#FFFFFF) by default, or accepts any params
-
# supported by #parse.
-
1
def initialize(*args)
-
344
@r = 255
-
344
@g = 255
-
344
@b = 255
-
344
@a = 255
-
-
344
if args.size.between?(3,4)
-
116
self.r = args[0]
-
116
self.g = args[1]
-
116
self.b = args[2]
-
116
self.a = args[3] if args[3]
-
else
-
228
set(*args)
-
end
-
end
-
-
# All-purpose setter - pass in another Color, '#000000', rgb vals... whatever
-
1
def set(*args)
-
228
val = Color.parse(*args)
-
228
unless val.nil?
-
228
self.r = val.r
-
228
self.g = val.g
-
228
self.b = val.b
-
228
self.a = val.a
-
end
-
228
self
-
end
-
-
# Test for equality, accepts string vals as well, eg Color.new('aaa') == '#AAAAAA' => true
-
1
def ==(val)
-
val = Color.parse(val)
-
return false if val.nil?
-
return r == val.r && g == val.g && b == val.b && a == val.a
-
end
-
-
# Setters for individual channels - take 0-255 or '00'-'FF' values
-
459
def r=(val); @r = from_hex(val); end
-
459
def g=(val); @g = from_hex(val); end
-
459
def b=(val); @b = from_hex(val); end
-
343
def a=(val); @a = from_hex(val); end
-
-
# Attempt to read in a string and parse it into values
-
1
def self.parse(*args)
-
228
case args.size
-
-
when 0 then
-
return nil
-
-
when 1 then
-
228
val = args[0]
-
-
# Trivial parse... :-)
-
228
return val if val.is_a?(Color)
-
-
# Single value, assume grayscale
-
114
return Color.new(val, val, val) if val.is_a?(Fixnum)
-
-
# Assume string
-
114
str = val.to_s.upcase
-
114
str = str[/[0-9A-F]{3,8}/] || ''
-
114
case str.size
-
when 3, 4 then
-
r, g, b, a = str.scan(/[0-9A-F]/)
-
when 6,8 then
-
114
r, g, b, a = str.scan(/[0-9A-F]{2}/)
-
else
-
return nil
-
end
-
-
114
return Color.new(r,g,b,a || 255)
-
-
when 3,4 then
-
return Color.new(*args)
-
-
end
-
nil
-
end
-
-
1
def inspect
-
to_s(true)
-
end
-
-
1
def to_s(add_hash = true)
-
114
trans? ? to_rgba(add_hash) : to_rgb(add_hash)
-
end
-
-
1
def to_rgb(add_hash = true)
-
114
(add_hash ? '#' : '') + to_hex(r) + to_hex(g) + to_hex(b)
-
end
-
-
1
def to_rgba(add_hash = true)
-
to_rgb(add_hash) + to_hex(a)
-
end
-
-
1
def opaque?
-
@a == 255
-
end
-
-
1
def trans?
-
114
@a != 255
-
end
-
-
1
def grayscale?
-
@r == @g && @g == @b
-
end
-
-
# Lighten color towards white. 0.0 is a no-op, 1.0 will return #FFFFFF
-
1
def lighten(amt = BRIGHTNESS_DEFAULT)
-
114
return self if amt <= 0
-
114
return WHITE if amt >= 1.0
-
114
val = Color.new(self)
-
114
val.r += ((255-val.r) * amt).to_i
-
114
val.g += ((255-val.g) * amt).to_i
-
114
val.b += ((255-val.b) * amt).to_i
-
114
val
-
end
-
-
# In place version of #lighten
-
1
def lighten!(amt = BRIGHTNESS_DEFAULT)
-
set(lighten(amt))
-
self
-
end
-
-
# Darken a color towards full black. 0.0 is a no-op, 1.0 will return #000000
-
1
def darken(amt = BRIGHTNESS_DEFAULT)
-
return self if amt <= 0
-
return BLACK if amt >= 1.0
-
val = Color.new(self)
-
val.r -= (val.r * amt).to_i
-
val.g -= (val.g * amt).to_i
-
val.b -= (val.b * amt).to_i
-
val
-
end
-
-
# In place version of #darken
-
1
def darken!(amt = BRIGHTNESS_DEFAULT)
-
set(darken(amt))
-
self
-
end
-
-
# Convert to grayscale, using perception-based weighting
-
1
def grayscale
-
val = Color.new(self)
-
val.r = val.g = val.b = (0.2126 * val.r + 0.7152 * val.g + 0.0722 * val.b)
-
val
-
end
-
-
# In place version of #grayscale
-
1
def grayscale!
-
set(grayscale)
-
self
-
end
-
-
# Blend to a color amt % towards another color value, eg
-
# red.blend(blue, 0.5) will be purple, white.blend(black, 0.5) will be gray, etc.
-
1
def blend(other, amt)
-
other = Color.parse(other)
-
return Color.new(self) if amt <= 0 || other.nil?
-
return Color.new(other) if amt >= 1.0
-
val = Color.new(self)
-
val.r += ((other.r - val.r)*amt).to_i
-
val.g += ((other.g - val.g)*amt).to_i
-
val.b += ((other.b - val.b)*amt).to_i
-
val
-
end
-
-
# In place version of #blend
-
1
def blend!(other, amt)
-
set(blend(other, amt))
-
self
-
end
-
-
# Class-level version for explicit blends of two values, useful with constants
-
1
def self.blend(col1, col2, amt)
-
col1 = Color.parse(col1)
-
col2 = Color.parse(col2)
-
col1.blend(col2, amt)
-
end
-
-
1
protected
-
-
# Convert int to string hex, eg 255 => 'FF'
-
1
def to_hex(val)
-
342
HEXVAL[val / 16] + HEXVAL[val % 16]
-
end
-
-
# Convert int or string to int, eg 80 => 80, 'FF' => 255, '7' => 119
-
1
def from_hex(val)
-
1716
if val.is_a?(String)
-
# Double up if single char form
-
342
val = val + val if val.size == 1
-
# Convert to integer
-
342
val = val.hex
-
end
-
# Clamp
-
1716
val = 0 if val < 0
-
1716
val = 255 if val > 255
-
1716
val
-
end
-
-
1
public
-
-
# Some constants for general use
-
1
WHITE = Color.new(255,255,255).freeze
-
1
BLACK = Color.new(0,0,0).freeze
-
-
end
-
-
# "Global" method for creating Color objects, eg:
-
# new_color = rgb(params[:new_color])
-
# style="border: 1px solid <%= rgb(10,50,80).lighten %>"
-
1
def rgb(*args)
-
Color.parse(*args)
-
end
-
1
module_function :rgb
-
end